Repository: grafana/terraform-provider-grafana

Issue:
Creation of k6 resources ( project + role assignments)
### Feature Request

Hey there,

we would like to create K6 projects and assign teams permissions to these projects.

I was not able to find the matching terraform resources yet, is that something on the roadmap?

The REST API is here: https://grafana.com/docs/grafana-cloud/testing/k6/reference/cloud-rest-api/v6/projects/#create-a-project


Discussion:
@jan-mrm: Hey there @joanlopez , thanks for implementing this! 👍
Does this also enable us to manage the permissions of a k6 project via terraform or was there a resource that could do that already? If not then we could open a new issue for that 🙂@joanlopez: If I'm not wrong, the permissions of a k6 project are managed through Grafana folders, and to do so, in theory, you should be able to use that attribute, which is available in the provider, yeah: [`grafana_folder_uid`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/data-sources/k6_project#grafana_folder_uid-1).

That said, if you still miss any concrete feature/operation, feel free to create a new issue and ping/assign me.@jan-mrm: Cool thanks. We will try that or open another feature request.

Solution patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 8be7d14a..a53a28d5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -8,6 +8,7 @@
 /internal/resources/connections/*                   @grafana/platform-monitoring @grafana/middleware-apps
 /internal/resources/fleetmanagement/*               @grafana/platform-monitoring @grafana/fleet-management-backend
 /internal/resources/frontendo11y/*                  @grafana/platform-monitoring @grafana/frontend-o11y
+/internal/resources/k6/*                            @grafana/platform-monitoring @grafana/k6-core
 /internal/resources/machinelearning/*               @grafana/platform-monitoring @grafana/machine-learning
 /internal/resources/oncall/*                        @grafana/platform-monitoring @grafana/grafana-irm-backend
 /internal/resources/slo/*                           @grafana/platform-monitoring @grafana/slo-squad
diff --git a/.github/workflows/acc-tests.yml b/.github/workflows/acc-tests.yml
index 3220603f..07df9070 100644
--- a/.github/workflows/acc-tests.yml
+++ b/.github/workflows/acc-tests.yml
@@ -8,17 +8,20 @@ on:
 # These permissions are needed to assume roles from Github's OIDC.
 permissions:
   contents: read
-  id-token: write
 
 jobs:
   # Tests that Terraform can actually install and use the provider
   # Ex: https://github.com/grafana/terraform-provider-grafana/issues/1372
   integration:
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
-        with: 
+        with:
           go-version-file: go.mod
       - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
       - run: make integration-test
@@ -28,19 +31,26 @@ jobs:
       group: cloud-instance
       cancel-in-progress: false
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      id-token: write
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
-        with: 
+        with:
           go-version-file: go.mod
       - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
       - name: Get Secrets
-        uses: grafana/shared-workflows/actions/get-vault-secrets@main
+        uses: grafana/shared-workflows/actions/get-vault-secrets@75804962c1ba608148988c1e2dc35fbb0ee21746
         with:
           repo_secrets: |
             GRAFANA_AUTH=cloud-instance-tests:auth
+            GRAFANA_STACK_ID=cloud-instance-k6-tests:stack-id
             GRAFANA_ONCALL_ACCESS_TOKEN=cloud-instance-tests:oncall-token
             GRAFANA_SM_ACCESS_TOKEN=cloud-instance-tests:sm-token
+            GRAFANA_K6_ACCESS_TOKEN=cloud-instance-k6-tests:k6-token
             GRAFANA_SM_URL=cloud-instance-tests:sm-url
             GRAFANA_URL=cloud-instance-tests:url
             GRAFANA_CLOUD_PROVIDER_URL=cloudprovider-tests:url
@@ -103,11 +113,18 @@ jobs:
             subset: 'generate'
     name: ${{ matrix.version }} - ${{ matrix.type }} - ${{ matrix.subset }}
     runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      id-token: write
     steps:
       - name: Check if fork
         if: github.event_name == 'pull_request'
+        env:
+          REPO_FULL_NAME: ${{ github.repository }}
+          PR_REPO_FULL_NAME: ${{ github.event.pull_request.head.repo.full_name }}
+          MATRIX_TYPE: ${{ matrix.type }}
         run: |
-          if [[ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" && "${{ matrix.type }}" == "enterprise" ]]; then
+          if [ "$MATRIX_TYPE" = "enterprise" ] && [ "$PR_REPO_FULL_NAME" != "$REPO_FULL_NAME" ]; then
             echo "IS_FORK=true" >> $GITHUB_ENV
           else
             echo "IS_FORK=false" >> $GITHUB_ENV
@@ -118,12 +135,14 @@ jobs:
           echo "Skipping job because PR is from a fork"
           exit 0
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
-        with: 
+        with:
           go-version-file: go.mod
       - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
       - name: Get Enterprise License
-        uses: grafana/shared-workflows/actions/get-vault-secrets@main
+        uses: grafana/shared-workflows/actions/get-vault-secrets@75804962c1ba608148988c1e2dc35fbb0ee21746
         if: matrix.type == 'enterprise'
         with:
           repo_secrets: |
diff --git a/.github/workflows/cloud-acc-tests.yml b/.github/workflows/cloud-acc-tests.yml
index 22fe727c..c568ab8c 100644
--- a/.github/workflows/cloud-acc-tests.yml
+++ b/.github/workflows/cloud-acc-tests.yml
@@ -24,12 +24,14 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
         with: 
           go-version-file: go.mod
       - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
       - name: Get Secrets
-        uses: grafana/shared-workflows/actions/get-vault-secrets@main
+        uses: grafana/shared-workflows/actions/get-vault-secrets@75804962c1ba608148988c1e2dc35fbb0ee21746
         with:
           repo_secrets: |
             GRAFANA_CLOUD_ACCESS_POLICY_TOKEN=cloud-tests:api-key
diff --git a/.github/workflows/comment-on-pr.yml b/.github/workflows/comment-on-pr.yml
index e344a47a..12da9802 100644
--- a/.github/workflows/comment-on-pr.yml
+++ b/.github/workflows/comment-on-pr.yml
@@ -1,14 +1,18 @@
 name: pr comment
 
 on:
-  pull_request_target:
+  pull_request:
 
 jobs:
   info-comment:
     runs-on: ubuntu-latest
     permissions:
       pull-requests: write
+      contents: read
     steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
         with:
           message: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e5768b59..d34fc9b6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,6 +14,12 @@ on:
   push:
     tags:
     - 'v*'
+
+permissions:
+  contents: write  # for goreleaser to create releases
+  id-token: write  # for signing
+  packages: write  # for publishing packages
+
 jobs:
   # Cloud tests don't run on every PR. We need to be sure they all pass before we release.
   # Temporarily disabling for release of 3.8.0 as the tests are flakey.
@@ -26,12 +32,15 @@ jobs:
     steps:
     - name: Checkout
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      with:
+        persist-credentials: false
     - name: Unshallow
       run: git fetch --prune --unshallow
     - name: Set up Go
       uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
       with:
         go-version-file: go.mod
+        cache: false
     - name: Import GPG key
       id: import_gpg
       uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 7a5e50ff..aa66a379 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -5,12 +5,18 @@ on:
     branches:
       - main
     
+permissions:
+  contents: read
+  pull-requests: read
+
 jobs:
   lint: 
     name: golangci-lint
     runs-on: ubuntu-latest
     steps: 
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - run: make golangci-lint # Using the makefile to have the same command in CI and locally
 
   terraform_fmt:
@@ -18,6 +24,8 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
       - name: terraform fmt
         run: terraform fmt -recursive -check || (echo "Terraform files aren't formatted. Run 'terraform fmt -recursive && go generate ./...'"; exit 1;)
@@ -27,6 +35,8 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
         with: 
           go-version-file: go.mod
@@ -46,6 +56,8 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          persist-credentials: false
       - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
         with: 
           go-version-file: go.mod
diff --git a/docs/data-sources/k6_load_test.md b/docs/data-sources/k6_load_test.md
new file mode 100644
index 00000000..9dc5e8c4
--- /dev/null
+++ b/docs/data-sources/k6_load_test.md
@@ -0,0 +1,49 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_load_test Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves a k6 load test.
+---
+
+# grafana_k6_load_test (Data Source)
+
+Retrieves a k6 load test.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+}
+
+data "grafana_k6_load_test" "from_id" {
+  id = grafana_k6_load_test.test_load_test.id
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `id` (Number) Numeric identifier of the load test.
+
+### Read-Only
+
+- `baseline_test_run_id` (Number) Identifier of a baseline test run used for results comparison.
+- `created` (String) The date when the load test was created.
+- `name` (String) Human-friendly identifier of the load test.
+- `project_id` (Number) The identifier of the project this load test belongs to.
+- `script` (String) The k6 test script content.
+- `updated` (String) The date when the load test was last updated.
diff --git a/docs/data-sources/k6_load_tests.md b/docs/data-sources/k6_load_tests.md
new file mode 100644
index 00000000..03f15f16
--- /dev/null
+++ b/docs/data-sources/k6_load_tests.md
@@ -0,0 +1,94 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_load_tests Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves all k6 load tests that belong to a project.
+---
+
+# grafana_k6_load_tests (Data Source)
+
+Retrieves all k6 load tests that belong to a project.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+
+  depends_on = [
+    grafana_k6_project.load_test_project,
+  ]
+}
+
+resource "grafana_k6_load_test" "test_load_test_2" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test (2)"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test,
+  ]
+}
+
+data "grafana_k6_load_tests" "from_project_id" {
+  project_id = grafana_k6_project.load_test_project.id
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test,
+    grafana_k6_load_test.test_load_test_2
+  ]
+}
+
+data "grafana_k6_load_tests" "filter_by_name" {
+  name       = "Terraform Test Load Test (2)"
+  project_id = grafana_k6_project.load_test_project.id
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test_2,
+  ]
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `project_id` (Number) The identifier of the project the load tests belong to.
+
+### Optional
+
+- `name` (String) Human-friendly identifier of the load test.
+
+### Read-Only
+
+- `id` (Number) The identifier of the project the load tests belong to. This is set to the same as the project_id.
+- `load_tests` (List of Object) (see [below for nested schema](#nestedatt--load_tests))
+
+<a id="nestedatt--load_tests"></a>
+### Nested Schema for `load_tests`
+
+Read-Only:
+
+- `baseline_test_run_id` (Number)
+- `created` (String)
+- `id` (Number)
+- `name` (String)
+- `project_id` (Number)
+- `script` (String)
+- `updated` (String)
diff --git a/docs/data-sources/k6_project.md b/docs/data-sources/k6_project.md
new file mode 100644
index 00000000..c7de1b10
--- /dev/null
+++ b/docs/data-sources/k6_project.md
@@ -0,0 +1,41 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_project Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves a k6 project.
+---
+
+# grafana_k6_project (Data Source)
+
+Retrieves a k6 project.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "test" {
+  name = "Terraform Test Project"
+}
+
+data "grafana_k6_project" "from_id" {
+  depends_on = [
+    grafana_k6_project.test
+  ]
+  id = grafana_k6_project.test.id
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `id` (Number) Numeric identifier of the project.
+
+### Read-Only
+
+- `created` (String) The date when the project was created.
+- `grafana_folder_uid` (String) The Grafana folder uid.
+- `is_default` (Boolean) Whether this project is the default for running tests when no explicit project identifier is provided.
+- `name` (String) Human-friendly identifier of the project.
+- `updated` (String) The date when the project was last updated.
diff --git a/docs/data-sources/k6_project_limits.md b/docs/data-sources/k6_project_limits.md
new file mode 100644
index 00000000..0f7092d0
--- /dev/null
+++ b/docs/data-sources/k6_project_limits.md
@@ -0,0 +1,38 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_project_limits Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves a k6 project limits.
+---
+
+# grafana_k6_project_limits (Data Source)
+
+Retrieves a k6 project limits.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "test_project_limits" {
+  name = "Terraform Project Test Limits"
+}
+
+data "grafana_k6_project_limits" "from_project_id" {
+  project_id = grafana_k6_project.test_project_limits.id
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `project_id` (Number) The identifier of the project to get limits for.
+
+### Read-Only
+
+- `duration_max_per_test` (Number) Maximum duration of a test in seconds.
+- `id` (Number) The identifier of the project limits. This is set to the same as the project_id.
+- `vu_browser_max_per_test` (Number) Maximum number of concurrent browser virtual users (VUs) used in one test.
+- `vu_max_per_test` (Number) Maximum number of concurrent virtual users (VUs) used in one test.
+- `vuh_max_per_month` (Number) Maximum amount of virtual user hours (VU/h) used per one calendar month.
diff --git a/docs/data-sources/k6_projects.md b/docs/data-sources/k6_projects.md
new file mode 100644
index 00000000..d3347524
--- /dev/null
+++ b/docs/data-sources/k6_projects.md
@@ -0,0 +1,57 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_projects Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves all k6 projects with the given name.
+---
+
+# grafana_k6_projects (Data Source)
+
+Retrieves all k6 projects with the given name.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "project" {
+  name = "Terraform Test Project"
+}
+
+resource "grafana_k6_project" "project_2" {
+  name = "Terraform Test Project"
+}
+
+data "grafana_k6_projects" "from_name" {
+  name = "Terraform Test Project"
+
+  depends_on = [
+    grafana_k6_project.project,
+    grafana_k6_project.project_2
+  ]
+
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Optional
+
+- `name` (String) Human-friendly identifier of the project.
+
+### Read-Only
+
+- `id` (String) Human-friendly identifier of the project. This is set to the same as name.
+- `projects` (List of Object) (see [below for nested schema](#nestedatt--projects))
+
+<a id="nestedatt--projects"></a>
+### Nested Schema for `projects`
+
+Read-Only:
+
+- `created` (String)
+- `grafana_folder_uid` (String)
+- `id` (Number)
+- `is_default` (Boolean)
+- `name` (String)
+- `updated` (String)
diff --git a/docs/index.md b/docs/index.md
index 468be594..a3cce720 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -280,6 +280,8 @@ provider "grafana" {
 - `frontend_o11y_api_access_token` (String, Sensitive) A Grafana Frontend Observability API access token. May alternatively be set via the `GRAFANA_FRONTEND_O11Y_API_ACCESS_TOKEN` environment variable.
 - `http_headers` (Map of String, Sensitive) Optional. HTTP headers mapping keys to values used for accessing the Grafana and Grafana Cloud APIs. May alternatively be set via the `GRAFANA_HTTP_HEADERS` environment variable in JSON format.
 - `insecure_skip_verify` (Boolean) Skip TLS certificate verification. May alternatively be set via the `GRAFANA_INSECURE_SKIP_VERIFY` environment variable.
+- `k6_access_token` (String, Sensitive) The k6 Cloud API token. May alternatively be set via the `GRAFANA_K6_ACCESS_TOKEN` environment variable.
+- `k6_url` (String) The k6 Cloud API url. May alternatively be set via the `GRAFANA_K6_URL` environment variable.
 - `oncall_access_token` (String, Sensitive) A Grafana OnCall access token. May alternatively be set via the `GRAFANA_ONCALL_ACCESS_TOKEN` environment variable.
 - `oncall_url` (String) An Grafana OnCall backend address. May alternatively be set via the `GRAFANA_ONCALL_URL` environment variable.
 - `org_id` (Number) The Grafana org ID, if you are using a self-hosted OSS or enterprise Grafana instance. May alternatively be set via the `GRAFANA_ORG_ID` environment variable.
diff --git a/docs/resources/apps_dashboard_dashboard_v1beta1.md b/docs/resources/apps_dashboard_dashboard_v1beta1.md
index 79b1c45e..768004a4 100644
--- a/docs/resources/apps_dashboard_dashboard_v1beta1.md
+++ b/docs/resources/apps_dashboard_dashboard_v1beta1.md
@@ -3,12 +3,16 @@
 page_title: "grafana_apps_dashboard_dashboard_v1beta1 Resource - terraform-provider-grafana"
 subcategory: "Grafana Apps"
 description: |-
-  Manages Grafana dashboards via the new Grafana App Platform API. This resource is currently EXPERIMENTAL and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+  Manages Grafana dashboards using the new Grafana APIs.
+  Official documentation https://grafana.com/docs/grafana/latest/dashboards/HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis
 ---
 
 # grafana_apps_dashboard_dashboard_v1beta1 (Resource)
 
-Manages Grafana dashboards via the new Grafana App Platform API. This resource is currently **EXPERIMENTAL** and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+Manages Grafana dashboards using the new Grafana APIs.
+
+* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/)
+* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis)
 
 
 
diff --git a/docs/resources/apps_playlist_playlist_v0alpha1.md b/docs/resources/apps_playlist_playlist_v0alpha1.md
index 80bdfb3d..b21ee941 100644
--- a/docs/resources/apps_playlist_playlist_v0alpha1.md
+++ b/docs/resources/apps_playlist_playlist_v0alpha1.md
@@ -3,12 +3,16 @@
 page_title: "grafana_apps_playlist_playlist_v0alpha1 Resource - terraform-provider-grafana"
 subcategory: "Grafana Apps"
 description: |-
-  Manages Grafana playlists via the new Grafana App Platform API. This resource is currently EXPERIMENTAL and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+  Manages Grafana playlists using the new Grafana APIs.
+  Official documentation https://grafana.com/docs/grafana/latest/dashboards/create-manage-playlists/HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/apis/
 ---
 
 # grafana_apps_playlist_playlist_v0alpha1 (Resource)
 
-Manages Grafana playlists via the new Grafana App Platform API. This resource is currently **EXPERIMENTAL** and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+Manages Grafana playlists using the new Grafana APIs.
+
+* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/create-manage-playlists/)
+* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/apis/)
 
 
 
diff --git a/docs/resources/contact_point.md b/docs/resources/contact_point.md
index 273d6503..c3a21660 100644
--- a/docs/resources/contact_point.md
+++ b/docs/resources/contact_point.md
@@ -525,6 +525,7 @@ Optional:
 - `message` (String) Custom message. You can use template variables.
 - `settings` (Map of String, Sensitive) Additional custom properties to attach to the notifier. Defaults to `map[]`.
 - `title` (String) Templated title of the message.
+- `tls_config` (Map of String, Sensitive) Allows configuring TLS for the webhook notifier.
 
 Read-Only:
 
diff --git a/docs/resources/k6_installation.md b/docs/resources/k6_installation.md
new file mode 100644
index 00000000..298a82f1
--- /dev/null
+++ b/docs/resources/k6_installation.md
@@ -0,0 +1,114 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_installation Resource - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Sets up the k6 App on a Grafana Cloud instance and generates a token.
+  Once a Grafana Cloud stack is created, a user can either use this resource or go into the UI to install k6.
+  This resource cannot be imported but it can be used on an existing k6 App installation without issues.
+  Note that this resource must be used on a provider configured with Grafana Cloud credentials.
+  Official documentation https://grafana.com/docs/grafana-cloud/testing/k6/
+  Required access policy scopes:
+  stacks:readstacks:writesubscriptions:readorgs:read
+---
+
+# grafana_k6_installation (Resource)
+
+Sets up the k6 App on a Grafana Cloud instance and generates a token. 
+Once a Grafana Cloud stack is created, a user can either use this resource or go into the UI to install k6.
+This resource cannot be imported but it can be used on an existing k6 App installation without issues.
+
+**Note that this resource must be used on a provider configured with Grafana Cloud credentials.**
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/testing/k6/)
+
+Required access policy scopes:
+
+* stacks:read
+* stacks:write
+* subscriptions:read
+* orgs:read
+
+## Example Usage
+
+```terraform
+variable "cloud_access_policy_token" {
+  description = "Cloud Access Policy token for Grafana Cloud with the following scopes: stacks:read|write|delete, stack-service-accounts:write"
+}
+variable "stack_slug" {}
+variable "cloud_region" {
+  default = "us"
+}
+
+// Step 1: Create a stack in Grafana Cloud
+provider "grafana" {
+  alias                     = "cloud"
+  cloud_access_policy_token = var.cloud_access_policy_token
+}
+
+resource "grafana_cloud_stack" "k6_stack" {
+  provider = grafana.cloud
+
+  name        = var.stack_slug
+  slug        = var.stack_slug
+  region_slug = var.cloud_region
+}
+
+// Step 2: Create a Service Account and a token to install the k6 App
+resource "grafana_cloud_stack_service_account" "k6_sa" {
+  provider   = grafana.cloud
+  stack_slug = var.stack_slug
+
+  name        = "${var.stack_slug}-k6-app"
+  role        = "Admin"
+  is_disabled = false
+}
+
+resource "grafana_cloud_stack_service_account_token" "k6_sa_token" {
+  provider   = grafana.cloud
+  stack_slug = var.stack_slug
+
+  name               = "${var.stack_slug}-k6-app-token"
+  service_account_id = grafana_cloud_stack_service_account.k6_sa.id
+}
+
+// Step 3: Install the k6 App on the stack
+resource "grafana_k6_installation" "k6_installation" {
+  provider = grafana.cloud
+
+  cloud_access_policy_token = var.cloud_access_policy_token
+  stack_id                  = grafana_cloud_stack.k6_stack.id
+  grafana_sa_token          = grafana_cloud_stack_service_account_token.k6_sa_token.key
+  grafana_user              = "admin"
+}
+
+// Step 4: Interact with the k6 App: create a new project
+provider "grafana" {
+  alias = "k6"
+
+  stack_id        = grafana_cloud_stack.k6_stack.id
+  k6_access_token = grafana_k6_installation.k6_installation.k6_access_token
+}
+
+resource "grafana_k6_project" "my_k6_project" {
+  provider = grafana.k6
+
+  name = "k6 Project created with TF"
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `cloud_access_policy_token` (String, Sensitive) The [Grafana Cloud access policy](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/).
+- `grafana_sa_token` (String, Sensitive) The [service account](https://grafana.com/docs/grafana/latest/administration/service-accounts/) token.
+- `grafana_user` (String) The user to use for the installation.
+- `stack_id` (String) The identifier of the stack to install k6 on.
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+- `k6_access_token` (String, Sensitive) Generated token to access the k6 API.
+- `k6_organization` (String) The identifier of the k6 organization.
diff --git a/docs/resources/k6_load_test.md b/docs/resources/k6_load_test.md
new file mode 100644
index 00000000..2f4a69c6
--- /dev/null
+++ b/docs/resources/k6_load_test.md
@@ -0,0 +1,56 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_load_test Resource - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Manages a k6 load test.
+---
+
+# grafana_k6_load_test (Resource)
+
+Manages a k6 load test.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `name` (String) Human-friendly identifier of the load test.
+- `project_id` (Number) The identifier of the project this load test belongs to.
+- `script` (String) The k6 test script content. Can be provided inline or via the `file()` function.
+
+### Optional
+
+- `baseline_test_run_id` (Number) Identifier of a baseline test run used for results comparison.
+
+### Read-Only
+
+- `created` (String) The date when the load test was created.
+- `id` (Number) Numeric identifier of the load test.
+- `updated` (String) The date when the load test was last updated.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import grafana_k6_load_test.name "{{ id }}"
+```
diff --git a/docs/resources/k6_project.md b/docs/resources/k6_project.md
new file mode 100644
index 00000000..33f78294
--- /dev/null
+++ b/docs/resources/k6_project.md
@@ -0,0 +1,42 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_project Resource - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Manages a k6 project.
+---
+
+# grafana_k6_project (Resource)
+
+Manages a k6 project.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "test_project" {
+  name = "Terraform Test Project"
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `name` (String) Human-friendly identifier of the project.
+
+### Read-Only
+
+- `created` (String) The date when the project was created.
+- `grafana_folder_uid` (String) The Grafana folder uid.
+- `id` (Number) Numeric identifier of the project.
+- `is_default` (Boolean) Use this project as default for running tests when no explicit project identifier is provided.
+- `updated` (String) The date when the project was last updated.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import grafana_k6_project.name "{{ id }}"
+```
diff --git a/docs/resources/k6_project_limits.md b/docs/resources/k6_project_limits.md
new file mode 100644
index 00000000..65f33237
--- /dev/null
+++ b/docs/resources/k6_project_limits.md
@@ -0,0 +1,53 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_project_limits Resource - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Manages limits for a k6 project.
+---
+
+# grafana_k6_project_limits (Resource)
+
+Manages limits for a k6 project.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "test_project_limits" {
+  name = "Terraform Project Test Limits"
+}
+
+resource "grafana_k6_project_limits" "test_limits" {
+  project_id              = grafana_k6_project.test_project_limits.id
+  vuh_max_per_month       = 1000
+  vu_max_per_test         = 100
+  vu_browser_max_per_test = 10
+  duration_max_per_test   = 3600
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `project_id` (Number) The identifier of the project to manage limits for.
+
+### Optional
+
+- `duration_max_per_test` (Number) Maximum duration of a test in seconds.
+- `vu_browser_max_per_test` (Number) Maximum number of concurrent browser virtual users (VUs) used in one test.
+- `vu_max_per_test` (Number) Maximum number of concurrent virtual users (VUs) used in one test.
+- `vuh_max_per_month` (Number) Maximum amount of virtual user hours (VU/h) used per one calendar month.
+
+### Read-Only
+
+- `id` (Number) The identifier of the project limits. This is the same as the project_id.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import grafana_k6_project_limits.name "{{ project_id }}"
+```
diff --git a/docs/resources/rule_group.md b/docs/resources/rule_group.md
index e0bc0f1a..f35051c9 100644
--- a/docs/resources/rule_group.md
+++ b/docs/resources/rule_group.md
@@ -142,6 +142,7 @@ Optional:
 - `exec_err_state` (String) Describes what state to enter when the rule's query is invalid and the rule cannot be executed. Options are OK, Error, KeepLast, and Alerting.  Defaults to Alerting if not set.
 - `for` (String) The amount of time for which the rule must be breached for the rule to be considered to be Firing. Before this time has elapsed, the rule is only considered to be Pending. Defaults to `0`.
 - `is_paused` (Boolean) Sets whether the alert should be paused or not. Defaults to `false`.
+- `keep_firing_for` (String) The amount of time for which the rule will considered to be Recovering after initially Firing. Before this time has elapsed, the rule will continue to fire once it's been triggered.
 - `labels` (Map of String) Key-value pairs to attach to the alert rule that can be used in matching, grouping, and routing. Defaults to `map[]`.
 - `no_data_state` (String) Describes what state to enter when the rule's query returns No Data. Options are OK, NoData, KeepLast, and Alerting. Defaults to NoData if not set.
 - `notification_settings` (Block List, Max: 1) Notification settings for the rule. If specified, it overrides the notification policies. Available since Grafana 10.4, requires feature flag 'alertingSimplifiedRouting' to be enabled. (see [below for nested schema](#nestedblock--rule--notification_settings))
diff --git a/examples/data-sources/grafana_k6_project/data-source.tf b/examples/data-sources/grafana_k6_project/data-source.tf
new file mode 100644
index 00000000..fcecc558
--- /dev/null
+++ b/examples/data-sources/grafana_k6_project/data-source.tf
@@ -0,0 +1,12 @@
+resource "grafana_k6_project" "test" {
+  name = "Terraform Test Project"
+}
+
+data "grafana_k6_project" "from_id" {
+  depends_on = [
+    grafana_k6_project.test
+  ]
+  id = grafana_k6_project.test.id
+}
+
+
diff --git a/examples/data-sources/grafana_k6_project_limits/data-source.tf b/examples/data-sources/grafana_k6_project_limits/data-source.tf
new file mode 100644
index 00000000..625a72bd
--- /dev/null
+++ b/examples/data-sources/grafana_k6_project_limits/data-source.tf
@@ -0,0 +1,7 @@
+resource "grafana_k6_project" "test_project_limits" {
+  name = "Terraform Project Test Limits"
+}
+
+data "grafana_k6_project_limits" "from_project_id" {
+  project_id = grafana_k6_project.test_project_limits.id
+}
\ No newline at end of file
diff --git a/examples/data-sources/grafana_k6_projects/data-source.tf b/examples/data-sources/grafana_k6_projects/data-source.tf
new file mode 100644
index 00000000..2708f97c
--- /dev/null
+++ b/examples/data-sources/grafana_k6_projects/data-source.tf
@@ -0,0 +1,19 @@
+resource "grafana_k6_project" "project" {
+  name = "Terraform Test Project"
+}
+
+resource "grafana_k6_project" "project_2" {
+  name = "Terraform Test Project"
+}
+
+data "grafana_k6_projects" "from_name" {
+  name = "Terraform Test Project"
+
+  depends_on = [
+    grafana_k6_project.project,
+    grafana_k6_project.project_2
+  ]
+
+}
+
+
diff --git a/examples/resources/grafana_contact_point/_acc_receiver_types.tf b/examples/resources/grafana_contact_point/_acc_receiver_types.tf
index b16748e6..cf5db0a9 100644
--- a/examples/resources/grafana_contact_point/_acc_receiver_types.tf
+++ b/examples/resources/grafana_contact_point/_acc_receiver_types.tf
@@ -169,6 +169,12 @@ resource "grafana_contact_point" "receiver_types" {
     max_alerts          = 100
     message             = "Custom message"
     title               = "Custom title"
+    tls_config = {
+      insecure_skip_verify = true
+      ca_certificate       = "ca.crt"
+      client_certificate   = "client.crt"
+      client_key           = "client.key"
+    }
   }
 
   wecom {
diff --git a/examples/resources/grafana_k6_installation/resource.tf b/examples/resources/grafana_k6_installation/resource.tf
new file mode 100644
index 00000000..15560f02
--- /dev/null
+++ b/examples/resources/grafana_k6_installation/resource.tf
@@ -0,0 +1,63 @@
+variable "cloud_access_policy_token" {
+  description = "Cloud Access Policy token for Grafana Cloud with the following scopes: stacks:read|write|delete, stack-service-accounts:write"
+}
+variable "stack_slug" {}
+variable "cloud_region" {
+  default = "us"
+}
+
+// Step 1: Create a stack in Grafana Cloud
+provider "grafana" {
+  alias                     = "cloud"
+  cloud_access_policy_token = var.cloud_access_policy_token
+}
+
+resource "grafana_cloud_stack" "k6_stack" {
+  provider = grafana.cloud
+
+  name        = var.stack_slug
+  slug        = var.stack_slug
+  region_slug = var.cloud_region
+}
+
+// Step 2: Create a Service Account and a token to install the k6 App
+resource "grafana_cloud_stack_service_account" "k6_sa" {
+  provider   = grafana.cloud
+  stack_slug = var.stack_slug
+
+  name        = "${var.stack_slug}-k6-app"
+  role        = "Admin"
+  is_disabled = false
+}
+
+resource "grafana_cloud_stack_service_account_token" "k6_sa_token" {
+  provider   = grafana.cloud
+  stack_slug = var.stack_slug
+
+  name               = "${var.stack_slug}-k6-app-token"
+  service_account_id = grafana_cloud_stack_service_account.k6_sa.id
+}
+
+// Step 3: Install the k6 App on the stack
+resource "grafana_k6_installation" "k6_installation" {
+  provider = grafana.cloud
+
+  cloud_access_policy_token = var.cloud_access_policy_token
+  stack_id                  = grafana_cloud_stack.k6_stack.id
+  grafana_sa_token          = grafana_cloud_stack_service_account_token.k6_sa_token.key
+  grafana_user              = "admin"
+}
+
+// Step 4: Interact with the k6 App: create a new project
+provider "grafana" {
+  alias = "k6"
+
+  stack_id        = grafana_cloud_stack.k6_stack.id
+  k6_access_token = grafana_k6_installation.k6_installation.k6_access_token
+}
+
+resource "grafana_k6_project" "my_k6_project" {
+  provider = grafana.k6
+
+  name = "k6 Project created with TF"
+}
diff --git a/examples/resources/grafana_k6_project/import.sh b/examples/resources/grafana_k6_project/import.sh
new file mode 100644
index 00000000..c34c9681
--- /dev/null
+++ b/examples/resources/grafana_k6_project/import.sh
@@ -0,0 +1 @@
+terraform import grafana_k6_project.name "{{ id }}"
diff --git a/examples/resources/grafana_k6_project/resource.tf b/examples/resources/grafana_k6_project/resource.tf
new file mode 100644
index 00000000..6683457b
--- /dev/null
+++ b/examples/resources/grafana_k6_project/resource.tf
@@ -0,0 +1,4 @@
+resource "grafana_k6_project" "test_project" {
+  name = "Terraform Test Project"
+}
+
diff --git a/examples/resources/grafana_k6_project_limits/import.sh b/examples/resources/grafana_k6_project_limits/import.sh
new file mode 100644
index 00000000..24bb07f6
--- /dev/null
+++ b/examples/resources/grafana_k6_project_limits/import.sh
@@ -0,0 +1 @@
+terraform import grafana_k6_project_limits.name "{{ project_id }}"
diff --git a/examples/resources/grafana_k6_project_limits/resource.tf b/examples/resources/grafana_k6_project_limits/resource.tf
new file mode 100644
index 00000000..e05c15a7
--- /dev/null
+++ b/examples/resources/grafana_k6_project_limits/resource.tf
@@ -0,0 +1,11 @@
+resource "grafana_k6_project" "test_project_limits" {
+  name = "Terraform Project Test Limits"
+}
+
+resource "grafana_k6_project_limits" "test_limits" {
+  project_id              = grafana_k6_project.test_project_limits.id
+  vuh_max_per_month       = 1000
+  vu_max_per_test         = 100
+  vu_browser_max_per_test = 10
+  duration_max_per_test   = 3600
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 2ffb5586..e80f57d1 100644
--- a/go.mod
+++ b/go.mod
@@ -13,10 +13,11 @@ require (
 	github.com/grafana/fleet-management-api v1.0.0
 	github.com/grafana/grafana-app-sdk v0.35.2-0.20250408075831-c2a87bde0849
 	github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20250214150112-a52892176c26
-	github.com/grafana/grafana-openapi-client-go v0.0.0-20241113095943-9cb2bbfeb8a3
+	github.com/grafana/grafana-openapi-client-go v0.0.0-20250424142317-beadd3136e10
 	github.com/grafana/grafana/apps/dashboard v0.0.0-20250424064802-2fbb2d6f5d27
 	github.com/grafana/grafana/apps/playlist v0.0.0-20250424064802-2fbb2d6f5d27
 	github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250424064802-2fbb2d6f5d27
+	github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20250416134020-d958828152cd
 	github.com/grafana/machine-learning-go-client v0.8.2
 	github.com/grafana/river v0.3.0
 	github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6
@@ -83,12 +84,12 @@ require (
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/analysis v0.23.0 // indirect
-	github.com/go-openapi/errors v0.22.0 // indirect
+	github.com/go-openapi/errors v0.22.1 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
 	github.com/go-openapi/loads v0.22.0 // indirect
 	github.com/go-openapi/spec v0.21.0 // indirect
-	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-openapi/swag v0.23.1 // indirect
 	github.com/go-openapi/validate v0.24.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
@@ -126,7 +127,7 @@ require (
 	github.com/klauspost/compress v1.18.0 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
 	github.com/magefile/mage v1.15.0 // indirect
-	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mailru/easyjson v0.9.0 // indirect
 	github.com/mattetti/filebuffer v1.0.1 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
diff --git a/go.sum b/go.sum
index 51779088..9917977c 100644
--- a/go.sum
+++ b/go.sum
@@ -106,8 +106,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
 github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
-github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
-github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
+github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU=
+github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
 github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
 github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@@ -120,8 +120,8 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
 github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
 github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
 github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
-github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
-github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
+github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
+github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
 github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
 github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
@@ -180,8 +180,8 @@ github.com/grafana/grafana-app-sdk/logging v0.35.1 h1:taVpl+RoixTYl0JBJGhH+fPVmw
 github.com/grafana/grafana-app-sdk/logging v0.35.1/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
 github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20250214150112-a52892176c26 h1:7NMB6/x0CcfH/zKQ5D+3Ffb2DbYMJBx0QdJ1GGdw8z4=
 github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20250214150112-a52892176c26/go.mod h1:sYWkB3NhyirQJfy3wtNQ29UYjoHbRlJlYhqN1jNsC5g=
-github.com/grafana/grafana-openapi-client-go v0.0.0-20241113095943-9cb2bbfeb8a3 h1:poKxGlUaEYVp2DMofC/I2GHw/vvtHAZ20c48I8rFB6M=
-github.com/grafana/grafana-openapi-client-go v0.0.0-20241113095943-9cb2bbfeb8a3/go.mod h1:hiZnMmXc9KXNUlvkV2BKFsiWuIFF/fF4wGgYWEjBitI=
+github.com/grafana/grafana-openapi-client-go v0.0.0-20250424142317-beadd3136e10 h1:RznghhbjMUEvGJD0p9AOCjnVOTq0MiINDt98BscNcdw=
+github.com/grafana/grafana-openapi-client-go v0.0.0-20250424142317-beadd3136e10/go.mod h1:hiZnMmXc9KXNUlvkV2BKFsiWuIFF/fF4wGgYWEjBitI=
 github.com/grafana/grafana-plugin-sdk-go v0.275.0 h1:icGmZG91lVqIo79w/pSki6N44d3IjOjTfsfQPfu4THU=
 github.com/grafana/grafana-plugin-sdk-go v0.275.0/go.mod h1:mO9LJqdXDh5JpO/xIdPAeg5LdThgQ06Y/SLpXDWKw2c=
 github.com/grafana/grafana/apps/dashboard v0.0.0-20250424064802-2fbb2d6f5d27 h1:UAv+x7lwteJR4pPMpIZ9hzngzAT1oiOQ55pq2GS4YCE=
@@ -190,6 +190,8 @@ github.com/grafana/grafana/apps/playlist v0.0.0-20250424064802-2fbb2d6f5d27 h1:a
 github.com/grafana/grafana/apps/playlist v0.0.0-20250424064802-2fbb2d6f5d27/go.mod h1:9U44mptAJW8bkvgPgCxsnki58/nz3wKPgDayeyeFWJs=
 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250424064802-2fbb2d6f5d27 h1:2Q158xAoyGgPVw7BIWF5cPYTkjjdsn9c8TpNA38gKzM=
 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250424064802-2fbb2d6f5d27/go.mod h1:kzjpaBODMbCSS2kvAnV43Pwxoq4lOxrgw/TGKqq8oTA=
+github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20250416134020-d958828152cd h1:UOHbve+keUInhIBSqA4m7GtF2wEiKyW8f/kUt9S0+zM=
+github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20250416134020-d958828152cd/go.mod h1:nBtSyJ0NTfJvEPSrRSKc1u7Vn0mB/kttEZdClHwTqi0=
 github.com/grafana/machine-learning-go-client v0.8.2 h1:TvU4e+Kgg4GhwBNYTMjBUNq4tbhcxe0L8w1eo/UfV2M=
 github.com/grafana/machine-learning-go-client v0.8.2/go.mod h1:GQKDn10CZqG11l1Qtc6BZ5V6e54fSv5Vi8wskWn3BWs=
 github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
@@ -305,8 +307,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
 github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
-github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
-github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
+github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
 github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
 github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
 github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
diff --git a/internal/common/client.go b/internal/common/client.go
index fc02f3bb..e1838590 100644
--- a/internal/common/client.go
+++ b/internal/common/client.go
@@ -10,17 +10,19 @@ import (
 	"github.com/grafana/grafana-app-sdk/k8s"
 	"github.com/grafana/grafana-com-public-clients/go/gcom"
 	goapi "github.com/grafana/grafana-openapi-client-go/client"
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
 	"github.com/grafana/machine-learning-go-client/mlapi"
 	"github.com/grafana/slo-openapi-client/go/slo"
 	SMAPI "github.com/grafana/synthetic-monitoring-api-go-client"
-	"github.com/grafana/terraform-provider-grafana/v3/internal/common/cloudproviderapi"
-	"github.com/grafana/terraform-provider-grafana/v3/internal/common/fleetmanagementapi"
 
-	"github.com/grafana/terraform-provider-grafana/v3/internal/common/frontendo11yapi"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/cloudproviderapi"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/common/connectionsapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/fleetmanagementapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/frontendo11yapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
 )
 
 type Client struct {
@@ -43,6 +45,9 @@ type Client struct {
 	FleetManagementClient *fleetmanagementapi.Client
 	FrontendO11yAPIClient *frontendo11yapi.Client
 
+	K6APIClient *k6.APIClient
+	K6APIConfig *k6providerapi.K6APIConfig
+
 	alertingMutex sync.Mutex
 }
 
diff --git a/internal/common/k6providerapi/config.go b/internal/common/k6providerapi/config.go
new file mode 100644
index 00000000..d63055e2
--- /dev/null
+++ b/internal/common/k6providerapi/config.go
@@ -0,0 +1,9 @@
+package k6providerapi
+
+type K6APIConfig struct {
+	// Token is the k6 Cloud API token.
+	Token string
+
+	// StackID is the k6 Cloud API stack id.
+	StackID int32
+}
diff --git a/internal/common/resource.go b/internal/common/resource.go
index ab51ef4d..6b5b323e 100644
--- a/internal/common/resource.go
+++ b/internal/common/resource.go
@@ -26,6 +26,7 @@ var (
 	CategoryConnections         ResourceCategory = "Connections"
 	CategoryFleetManagement     ResourceCategory = "Fleet Management"
 	CategoryFrontendO11y        ResourceCategory = "Frontend Observability"
+	CategoryK6                  ResourceCategory = "k6"
 )
 
 type ResourceCommon struct {
diff --git a/internal/common/types.go b/internal/common/types.go
new file mode 100644
index 00000000..18516fa9
--- /dev/null
+++ b/internal/common/types.go
@@ -0,0 +1,13 @@
+package common
+
+import (
+	"errors"
+	"math"
+)
+
+func ToInt32[T ~int | ~int64](val T) (int32, error) {
+	if val < math.MinInt32 || val > math.MaxInt32 {
+		return 0, errors.New("value overflows int32")
+	}
+	return int32(val), nil
+}
diff --git a/internal/resources/appplatform/dashboard_resource.go b/internal/resources/appplatform/dashboard_resource.go
index aee99936..6efdc67e 100644
--- a/internal/resources/appplatform/dashboard_resource.go
+++ b/internal/resources/appplatform/dashboard_resource.go
@@ -30,9 +30,11 @@ func Dashboard() NamedResource {
 			Schema: ResourceSpecSchema{
 				Description: "Manages Grafana dashboards.",
 				MarkdownDescription: `
-Manages Grafana dashboards via the new Grafana App Platform API. This resource is currently **EXPERIMENTAL** and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+Manages Grafana dashboards using the new Grafana APIs.
+
+* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/)
+* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis)
 	`,
-				DeprecationMessage: "This resource is currently EXPERIMENTAL and may be subject to change.",
 				SpecAttributes: map[string]schema.Attribute{
 					"json": schema.StringAttribute{
 						Required:    true,
diff --git a/internal/resources/appplatform/playlist_resource.go b/internal/resources/appplatform/playlist_resource.go
index 05bbed2b..8dc8d2ea 100644
--- a/internal/resources/appplatform/playlist_resource.go
+++ b/internal/resources/appplatform/playlist_resource.go
@@ -45,9 +45,11 @@ func Playlist() NamedResource {
 			Schema: ResourceSpecSchema{
 				Description: "Manages Grafana playlists.",
 				MarkdownDescription: `
-Manages Grafana playlists via the new Grafana App Platform API. This resource is currently **EXPERIMENTAL** and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+Manages Grafana playlists using the new Grafana APIs.
+
+* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/create-manage-playlists/)
+* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/apis/)
 `,
-				DeprecationMessage: "This resource is currently EXPERIMENTAL and may be subject to change.",
 				SpecAttributes: map[string]schema.Attribute{
 					"title": schema.StringAttribute{
 						Required:    true,
diff --git a/internal/resources/cloud/resource_k6_installation.go b/internal/resources/cloud/resource_k6_installation.go
new file mode 100644
index 00000000..1ce613c7
--- /dev/null
+++ b/internal/resources/cloud/resource_k6_installation.go
@@ -0,0 +1,175 @@
+package cloud
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+	"github.com/grafana/grafana-com-public-clients/go/gcom"
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+func resourceK6Installation() *common.Resource {
+	schema := &schema.Resource{
+
+		Description: `
+Sets up the k6 App on a Grafana Cloud instance and generates a token. 
+Once a Grafana Cloud stack is created, a user can either use this resource or go into the UI to install k6.
+This resource cannot be imported but it can be used on an existing k6 App installation without issues.
+
+**Note that this resource must be used on a provider configured with Grafana Cloud credentials.**
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/testing/k6/)
+
+Required access policy scopes:
+
+* stacks:read
+* stacks:write
+* subscriptions:read
+* orgs:read
+`,
+		CreateContext: withClient[schema.CreateContextFunc](resourceK6InstallationCreate),
+		ReadContext:   resourceK6InstallationRead,
+		DeleteContext: resourceK6InstallationDelete,
+
+		Schema: map[string]*schema.Schema{
+			"cloud_access_policy_token": {
+				Type:        schema.TypeString,
+				Sensitive:   true,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The [Grafana Cloud access policy](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/).",
+			},
+			"stack_id": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The identifier of the stack to install k6 on.",
+			},
+			"grafana_sa_token": {
+				Type:        schema.TypeString,
+				Sensitive:   true,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The [service account](https://grafana.com/docs/grafana/latest/administration/service-accounts/) token.",
+			},
+			"grafana_user": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The user to use for the installation.",
+			},
+			"k6_access_token": {
+				Type:        schema.TypeString,
+				Sensitive:   true,
+				Computed:    true,
+				Description: "Generated token to access the k6 API.",
+			},
+			"k6_organization": {
+				Type:        schema.TypeString,
+				Computed:    true,
+				Description: "The identifier of the k6 organization.",
+			},
+		},
+	}
+
+	return common.NewLegacySDKResource(
+		common.CategoryK6,
+		"grafana_k6_installation",
+		nil,
+		schema,
+	)
+}
+
+func resourceK6InstallationCreate(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
+	const url = "https://api.k6.io/v3/account/grafana-app/start"
+	req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, nil)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	stackID, ok := d.Get("stack_id").(string)
+	if !ok || len(stackID) == 0 {
+		return diag.Errorf("the grafana_k6_installation must have a valid stack_id")
+	}
+
+	cloudAccessPolicyToken, ok := d.Get("cloud_access_policy_token").(string)
+	if !ok || len(cloudAccessPolicyToken) == 0 {
+		return diag.Errorf("the grafana_k6_installation must have a valid cloud_access_policy_token")
+	}
+
+	grafanaServiceAccountToken, ok := d.Get("grafana_sa_token").(string)
+	if !ok || len(grafanaServiceAccountToken) == 0 {
+		return diag.Errorf("the grafana_k6_installation must have a valid grafana_sa_token")
+	}
+
+	grafanaUser, ok := d.Get("grafana_user").(string)
+	if !ok || len(grafanaUser) == 0 {
+		return diag.Errorf("the grafana_k6_installation must have a valid grafana_user")
+	}
+
+	req.Header.Set("X-Stack-Id", stackID)
+	req.Header.Set("X-Grafana-Key", cloudAccessPolicyToken)
+	req.Header.Set("X-Grafana-Service-Token", grafanaServiceAccountToken)
+	req.Header.Set("X-Grafana-User", grafanaUser)
+
+	resp, err := cloudClient.GetConfig().HTTPClient.Do(req)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	var installationRes struct {
+		V3GrafanaToken string `json:"v3_grafana_token"`
+		OrganizationID string `json:"organization_id"`
+	}
+
+	err = json.NewDecoder(resp.Body).Decode(&installationRes)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	d.SetId(installationRes.OrganizationID)
+
+	if err := d.Set("k6_access_token", installationRes.V3GrafanaToken); err != nil {
+		return diag.FromErr(err)
+	}
+
+	if err := d.Set("k6_organization", installationRes.OrganizationID); err != nil {
+		return diag.FromErr(err)
+	}
+
+	return resourceK6InstallationRead(ctx, d, nil)
+}
+
+// Management of the installation is a one-off operation. The state cannot be updated through a read operation.
+// This read function will only invalidate the state (forcing recreation) if the installation has been deleted.
+func resourceK6InstallationRead(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics {
+	var stackID int32
+	if intStackID, err := strconv.Atoi(d.Get("stack_id").(string)); err != nil {
+		return diag.Errorf("could not convert stack_id to integer: %s", err.Error())
+	} else if stackID, err = common.ToInt32(intStackID); err != nil {
+		return diag.Errorf("could not convert stack_id to int32: %s", err.Error())
+	}
+
+	tempClient := k6.NewAPIClient(k6.NewConfiguration())
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.Get("k6_access_token").(string))
+	if _, _, err := tempClient.ProjectsAPI.ProjectsList(ctx).XStackId(stackID).Execute(); err != nil {
+		return common.WarnMissing("k6 installation", d)
+	}
+
+	return nil
+}
+
+func resourceK6InstallationDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
+	// To be implemented, not supported yet
+	return nil
+}
diff --git a/internal/resources/cloud/resources.go b/internal/resources/cloud/resources.go
index f91dfa8c..ab8bea67 100644
--- a/internal/resources/cloud/resources.go
+++ b/internal/resources/cloud/resources.go
@@ -20,6 +20,7 @@ var Resources = []*common.Resource{
 	resourceStack(),
 	resourceStackServiceAccount(),
 	resourceStackServiceAccountToken(),
+	resourceK6Installation(),
 	resourceSyntheticMonitoringInstallation(),
 	resourcePDCNetwork(),
 	resourcePDCNetworkToken(),
diff --git a/internal/resources/grafana/resource_alerting_contact_point.go b/internal/resources/grafana/resource_alerting_contact_point.go
index 9f43d236..b99af794 100644
--- a/internal/resources/grafana/resource_alerting_contact_point.go
+++ b/internal/resources/grafana/resource_alerting_contact_point.go
@@ -3,6 +3,7 @@ package grafana
 import (
 	"context"
 	"fmt"
+	"log"
 	"reflect"
 	"strconv"
 	"time"
@@ -422,7 +423,7 @@ func packNotifierStringField(gfSettings, tfSettings *map[string]interface{}, gfK
 func packSecureFields(tfSettings, state map[string]interface{}, secureFields []string) {
 	for _, tfKey := range secureFields {
 		if v, ok := state[tfKey]; ok && v != nil {
-			tfSettings[tfKey] = v.(string)
+			tfSettings[tfKey] = v
 		}
 	}
 }
@@ -445,3 +446,67 @@ func getNotifierConfigFromStateWithUID(data *schema.ResourceData, n notifier, ui
 
 	return nil
 }
+
+func unpackTLSConfig(tfSettings, gfSettings map[string]interface{}) {
+	tlsConfig, ok := tfSettings["tls_config"].(map[string]any)
+	if !ok || len(tlsConfig) == 0 {
+		return
+	}
+
+	gfTLSConfig := make(map[string]any)
+
+	if is, ok := tlsConfig["insecure_skip_verify"].(string); ok {
+		if insecureSkipVerify, err := strconv.ParseBool(is); err != nil {
+			log.Printf("[WARN] failed to parse 'insecure_skip_verify': %s", err)
+		} else {
+			gfTLSConfig["insecureSkipVerify"] = insecureSkipVerify
+		}
+	}
+
+	if caCertificate, ok := tlsConfig["ca_certificate"].(string); ok {
+		gfTLSConfig["caCertificate"] = caCertificate
+	}
+
+	if clientCertificate, ok := tlsConfig["client_certificate"].(string); ok {
+		gfTLSConfig["clientCertificate"] = clientCertificate
+	}
+
+	if clientKey, ok := tlsConfig["client_key"].(string); ok {
+		gfTLSConfig["clientKey"] = clientKey
+	}
+
+	gfSettings["tlsConfig"] = gfTLSConfig
+}
+
+func packTLSConfig(gfSettings, tfSettings map[string]interface{}) {
+	tlsConfig, ok := gfSettings["tlsConfig"].(map[string]any)
+	if !ok || len(tlsConfig) == 0 {
+		return
+	}
+
+	tfTLSConfig := make(map[string]any)
+
+	if is, ok := tlsConfig["insecureSkipVerify"].(string); ok {
+		if insecureSkipVerify, err := strconv.ParseBool(is); err != nil {
+			log.Printf("[WARN] failed to parse 'insecure_skip_verify': %s", err)
+		} else {
+			tfTLSConfig["insecure_skip_verify"] = insecureSkipVerify
+		}
+	}
+
+	if caCertificate, ok := tlsConfig["caCertificate"].(string); ok {
+		tfTLSConfig["ca_certificate"] = caCertificate
+	}
+
+	if clientCertificate, ok := tlsConfig["clientCertificate"].(string); ok {
+		tfTLSConfig["client_certificate"] = clientCertificate
+	}
+
+	if clientKey, ok := tlsConfig["clientKey"].(string); ok {
+		tfTLSConfig["client_key"] = clientKey
+	}
+
+	delete(gfSettings, "tlsConfig")
+
+	tfSettings["tls_config"] = tfTLSConfig
+}
diff --git a/internal/resources/grafana/resource_alerting_contact_point_notifiers.go b/internal/resources/grafana/resource_alerting_contact_point_notifiers.go
index 1127bf85..e0283458 100644
--- a/internal/resources/grafana/resource_alerting_contact_point_notifiers.go
+++ b/internal/resources/grafana/resource_alerting_contact_point_notifiers.go
@@ -623,8 +623,7 @@ func (o lineNotifier) unpack(raw interface{}, name string) *models.EmbeddedConta
 	}
 }
 
-type oncallNotifier struct {
-}
+type oncallNotifier struct{}
 
 var _ notifier = (*oncallNotifier)(nil)
 
@@ -2112,7 +2111,7 @@ func (w webhookNotifier) meta() notifierMeta {
 		field:        "webhook",
 		typeStr:      "webhook",
 		desc:         "A contact point that sends notifications to an arbitrary webhook, using the Prometheus webhook format defined here: https://prometheus.io/docs/alerting/latest/configuration/#webhook_config",
-		secureFields: []string{"basic_auth_password", "authorization_credentials"},
+		secureFields: []string{"basic_auth_password", "authorization_credentials", "tls_config"},
 	}
 }
 
@@ -2165,6 +2164,12 @@ func (w webhookNotifier) schema() *schema.Resource {
 		Optional:    true,
 		Description: "Templated title of the message.",
 	}
+	r.Schema["tls_config"] = &schema.Schema{
+		Type:        schema.TypeMap,
+		Optional:    true,
+		Sensitive:   true,
+		Description: "Allows configuring TLS for the webhook notifier.",
+	}
 	return r
 }
 
@@ -2180,6 +2185,7 @@ func (w webhookNotifier) pack(p *models.EmbeddedContactPoint, data *schema.Resou
 	packNotifierStringField(&settings, &notifier, "authorization_credentials", "authorization_credentials")
 	packNotifierStringField(&settings, &notifier, "message", "message")
 	packNotifierStringField(&settings, &notifier, "title", "title")
+	packTLSConfig(settings, notifier)
 	if v, ok := settings["maxAlerts"]; ok && v != nil {
 		switch typ := v.(type) {
 		case int:
@@ -2216,6 +2222,7 @@ func (w webhookNotifier) unpack(raw interface{}, name string) *models.EmbeddedCo
 	unpackNotifierStringField(&json, &settings, "authorization_credentials", "authorization_credentials")
 	unpackNotifierStringField(&json, &settings, "message", "message")
 	unpackNotifierStringField(&json, &settings, "title", "title")
+	unpackTLSConfig(json, settings)
 	if v, ok := json["max_alerts"]; ok && v != nil {
 		switch typ := v.(type) {
 		case int:
diff --git a/internal/resources/grafana/resource_alerting_rule_group.go b/internal/resources/grafana/resource_alerting_rule_group.go
index 31085197..b9b2e2b4 100644
--- a/internal/resources/grafana/resource_alerting_rule_group.go
+++ b/internal/resources/grafana/resource_alerting_rule_group.go
@@ -104,6 +104,17 @@ This resource requires Grafana 9.1.0 or later.
 								return oldDuration == newDuration
 							},
 						},
+						"keep_firing_for": {
+							Type:             schema.TypeString,
+							Optional:         true,
+							Description:      "The amount of time for which the rule will considered to be Recovering after initially Firing. Before this time has elapsed, the rule will continue to fire once it's been triggered.",
+							ValidateDiagFunc: common.ValidateDurationWithDays,
+							DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool {
+								oldDuration, _ := strfmt.ParseDuration(oldValue)
+								newDuration, _ := strfmt.ParseDuration(newValue)
+								return oldDuration == newDuration
+							},
+						},
 						"no_data_state": {
 							Type:        schema.TypeString,
 							Optional:    true,
@@ -519,6 +530,10 @@ func packAlertRule(r *models.ProvisionedAlertRule) (interface{}, error) {
 		json["record"] = record
 	}
 
+	if r.KeepFiringFor != 0 {
+		json["keep_firing_for"] = r.KeepFiringFor.String()
+	}
+
 	return json, nil
 }
 
@@ -538,6 +553,15 @@ func unpackAlertRule(raw interface{}, groupName string, folderUID string, orgID
 		return nil, err
 	}
 
+	keepFiringForStr := json["keep_firing_for"].(string)
+	if keepFiringForStr == "" {
+		keepFiringForStr = "0"
+	}
+	keepFiringForDuration, err := strfmt.ParseDuration(keepFiringForStr)
+	if err != nil {
+		return nil, err
+	}
+
 	ns, err := unpackNotificationSettings(json["notification_settings"])
 	if err != nil {
 		return nil, err
@@ -555,6 +579,9 @@ func unpackAlertRule(raw interface{}, groupName string, folderUID string, orgID
 		if forDuration != 0 {
 			return nil, fmt.Errorf(incompatFieldMsgFmt, "for")
 		}
+		if keepFiringForDuration != 0 {
+			return nil, fmt.Errorf(incompatFieldMsgFmt, "keep_firing_for")
+		}
 		if noDataState != "" {
 			return nil, fmt.Errorf(incompatFieldMsgFmt, "no_data_state")
 		}
@@ -588,6 +615,7 @@ func unpackAlertRule(raw interface{}, groupName string, folderUID string, orgID
 		ExecErrState:         common.Ref(errState),
 		NoDataState:          common.Ref(noDataState),
 		For:                  common.Ref(strfmt.Duration(forDuration)),
+		KeepFiringFor:        strfmt.Duration(keepFiringForDuration),
 		Data:                 data,
 		Condition:            common.Ref(condition),
 		Labels:               unpackMap(json["labels"]),
diff --git a/internal/resources/grafana/resource_team.go b/internal/resources/grafana/resource_team.go
index e35e302b..b31df1ec 100644
--- a/internal/resources/grafana/resource_team.go
+++ b/internal/resources/grafana/resource_team.go
@@ -427,7 +427,7 @@ func memberChanges(stateMembers, configMembers map[string]TeamMember) []MemberCh
 func addMemberIdsToChanges(client *goapi.GrafanaHTTPAPI, changes []MemberChange) ([]MemberChange, error) {
 	gUserMap := make(map[string]int64)
 
-	resp, err := client.Org.GetOrgUsersForCurrentOrg()
+	resp, err := client.Org.GetOrgUsersForCurrentOrg(nil)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/resources/k6/common.go b/internal/resources/k6/common.go
new file mode 100644
index 00000000..6f67a5dd
--- /dev/null
+++ b/internal/resources/k6/common.go
@@ -0,0 +1,82 @@
+package k6
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+)
+
+type basePluginFrameworkResource struct {
+	client *k6.APIClient
+	config *k6providerapi.K6APIConfig
+}
+
+func (r *basePluginFrameworkResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	// Configure is called multiple times (sometimes when ProviderData is not yet available), we only want to configure once
+	if req.ProviderData == nil || r.client != nil {
+		return
+	}
+
+	client, ok := req.ProviderData.(*common.Client)
+	if !ok {
+		resp.Diagnostics.AddError(
+			"Unexpected Resource Configure Type",
+			fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+		)
+
+		return
+	}
+
+	if client.K6APIClient == nil || client.K6APIConfig == nil {
+		resp.Diagnostics.AddError(
+			"The Grafana Provider is missing a configuration for the k6 Cloud API.",
+			"Please ensure that k6_access_token and stack_id are set in the provider configuration.",
+		)
+
+		return
+	}
+
+	r.client = client.K6APIClient
+	r.config = client.K6APIConfig
+}
+
+type basePluginFrameworkDataSource struct {
+	client *k6.APIClient
+	config *k6providerapi.K6APIConfig
+}
+
+func (d *basePluginFrameworkDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	// Configure is called multiple times (sometimes when ProviderData is not yet available), we only want to configure once
+	if req.ProviderData == nil || d.client != nil {
+		return
+	}
+
+	client, ok := req.ProviderData.(*common.Client)
+	if !ok {
+		resp.Diagnostics.AddError(
+			"Unexpected Data Source Configure Type",
+			fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+		)
+
+		return
+	}
+
+	if client.K6APIClient == nil || client.K6APIConfig == nil {
+		resp.Diagnostics.AddError(
+			"The Grafana Provider is missing a configuration for the k6 Cloud API.",
+			"Please ensure that k6_access_token and stack_id are set in the provider configuration.",
+		)
+
+		return
+	}
+
+	d.client = client.K6APIClient
+	d.config = client.K6APIConfig
+}
diff --git a/internal/resources/k6/common_lister.go b/internal/resources/k6/common_lister.go
new file mode 100644
index 00000000..a4ef1c64
--- /dev/null
+++ b/internal/resources/k6/common_lister.go
@@ -0,0 +1,21 @@
+package k6
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+)
+
+// k6ListerFunction is a helper function that wraps a lister function be used more easily in k6 resources.
+func k6ListerFunction(listerFunc func(ctx context.Context, client *k6.APIClient, config *k6providerapi.K6APIConfig) ([]string, error)) common.ResourceListIDsFunc {
+	return func(ctx context.Context, client *common.Client, data any) ([]string, error) {
+		if client.K6APIClient == nil || client.K6APIConfig == nil {
+			return nil, fmt.Errorf("client not configured for the k6 Cloud API")
+		}
+		return listerFunc(ctx, client.K6APIClient, client.K6APIConfig)
+	}
+}
diff --git a/internal/resources/k6/data_source_k6_project.go b/internal/resources/k6/data_source_k6_project.go
new file mode 100644
index 00000000..9104de60
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_project.go
@@ -0,0 +1,124 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*projectDataSource)(nil)
+)
+
+var (
+	dataSourceProjectName = "grafana_k6_project"
+)
+
+func dataSourceProject() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceProjectName,
+		&projectDataSource{},
+	)
+}
+
+// projectDataSourceModel maps the data source schema data.
+type projectDataSourceModel struct {
+	ID               types.Int32  `tfsdk:"id"`
+	Name             types.String `tfsdk:"name"`
+	IsDefault        types.Bool   `tfsdk:"is_default"`
+	GrafanaFolderUID types.String `tfsdk:"grafana_folder_uid"`
+	Created          types.String `tfsdk:"created"`
+	Updated          types.String `tfsdk:"updated"`
+}
+
+// projectDataSource is the data source implementation.
+type projectDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *projectDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceProjectName
+}
+
+// Schema defines the schema for the data source.
+func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves a k6 project.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "Numeric identifier of the project.",
+				Required:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the project.",
+				Computed:    true,
+			},
+			"is_default": schema.BoolAttribute{
+				Description: "Whether this project is the default for running tests when no explicit project identifier is provided.",
+				Computed:    true,
+			},
+			"grafana_folder_uid": schema.StringAttribute{
+				Description: "The Grafana folder uid.",
+				Computed:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "The date when the project was created.",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "The date when the project was last updated.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state projectDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.ProjectsAPI.ProjectsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	p, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project",
+			"Could not read k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	state.Name = types.StringValue(p.GetName())
+	state.IsDefault = types.BoolValue(p.GetIsDefault())
+	state.GrafanaFolderUID = handleGrafanaFolderUID(p.GrafanaFolderUid)
+	state.Created = types.StringValue(p.GetCreated().Format(time.RFC3339Nano))
+	state.Updated = types.StringValue(p.GetUpdated().Format(time.RFC3339Nano))
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
+
+func handleGrafanaFolderUID(grafanaFolderUID k6.NullableString) types.String {
+	if !grafanaFolderUID.IsSet() {
+		return types.StringNull()
+	}
+	return types.StringValue(*grafanaFolderUID.Get())
+}
diff --git a/internal/resources/k6/data_source_k6_project_limits.go b/internal/resources/k6/data_source_k6_project_limits.go
new file mode 100644
index 00000000..b2515ddb
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_project_limits.go
@@ -0,0 +1,117 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*projectLimitsDataSource)(nil)
+)
+
+var (
+	dataSourceProjectLimitsName = "grafana_k6_project_limits"
+)
+
+func dataSourceProjectLimits() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceProjectLimitsName,
+		&projectLimitsDataSource{},
+	)
+}
+
+// projectLimitsDataSourceModel maps the data source schema data.
+type projectLimitsDataSourceModel struct {
+	ID                  types.Int32 `tfsdk:"id"`
+	ProjectID           types.Int32 `tfsdk:"project_id"`
+	VuhMaxPerMonth      types.Int32 `tfsdk:"vuh_max_per_month"`
+	VuMaxPerTest        types.Int32 `tfsdk:"vu_max_per_test"`
+	VuBrowserMaxPerTest types.Int32 `tfsdk:"vu_browser_max_per_test"`
+	DurationMaxPerTest  types.Int32 `tfsdk:"duration_max_per_test"`
+}
+
+// projectLimitsDataSource is the data source implementation.
+type projectLimitsDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *projectLimitsDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceProjectLimitsName
+}
+
+// Schema defines the schema for the data source.
+func (d *projectLimitsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves a k6 project limits.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "The identifier of the project limits. This is set to the same as the project_id.",
+				Computed:    true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project to get limits for.",
+				Required:    true,
+			},
+			"vuh_max_per_month": schema.Int32Attribute{
+				Description: "Maximum amount of virtual user hours (VU/h) used per one calendar month.",
+				Computed:    true,
+			},
+			"vu_max_per_test": schema.Int32Attribute{
+				Description: "Maximum number of concurrent virtual users (VUs) used in one test.",
+				Computed:    true,
+			},
+			"vu_browser_max_per_test": schema.Int32Attribute{
+				Description: "Maximum number of concurrent browser virtual users (VUs) used in one test.",
+				Computed:    true,
+			},
+			"duration_max_per_test": schema.Int32Attribute{
+				Description: "Maximum duration of a test in seconds.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *projectLimitsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state projectLimitsDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.ProjectsAPI.ProjectsLimitsRetrieve(ctx, state.ProjectID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	limits, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project limits",
+			"Could not read k6 project limits with project id "+strconv.Itoa(int(state.ProjectID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	state.ID = types.Int32Value(limits.GetProjectId())
+	state.ProjectID = types.Int32Value(limits.GetProjectId())
+	state.VuhMaxPerMonth = types.Int32Value(limits.GetVuhMaxPerMonth())
+	state.VuMaxPerTest = types.Int32Value(limits.GetVuMaxPerTest())
+	state.VuBrowserMaxPerTest = types.Int32Value(limits.GetVuBrowserMaxPerTest())
+	state.DurationMaxPerTest = types.Int32Value(limits.GetDurationMaxPerTest())
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/resources/k6/data_source_k6_projects.go b/internal/resources/k6/data_source_k6_projects.go
new file mode 100644
index 00000000..563193a0
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_projects.go
@@ -0,0 +1,131 @@
+package k6
+
+import (
+	"context"
+	"sort"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*projectsDataSource)(nil)
+)
+
+var (
+	dataSourceProjectsName = "grafana_k6_projects"
+)
+
+func dataSourceProjects() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceProjectsName,
+		&projectsDataSource{},
+	)
+}
+
+// projectsDataSourceModel maps the data source schema data.
+type projectsDataSourceModel struct {
+	ID       types.String             `tfsdk:"id"`
+	Name     types.String             `tfsdk:"name"`
+	Projects []projectDataSourceModel `tfsdk:"projects"`
+}
+
+// projectsDataSource is the data source implementation.
+type projectsDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *projectsDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceProjectsName
+}
+
+// Schema defines the schema for the data source.
+func (d *projectsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves all k6 projects with the given name.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: "Human-friendly identifier of the project. This is set to the same as name.",
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the project.",
+				Optional:    true,
+			},
+			"projects": schema.ListAttribute{
+				Computed: true,
+				ElementType: types.ObjectType{
+					AttrTypes: map[string]attr.Type{
+						"id":                 types.Int32Type,
+						"name":               types.StringType,
+						"is_default":         types.BoolType,
+						"grafana_folder_uid": types.StringType,
+						"created":            types.StringType,
+						"updated":            types.StringType,
+					},
+				},
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *projectsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state projectsDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the name
+	state.ID = state.Name
+
+	// Retrieve projects
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.ProjectsAPI.ProjectsList(ctx).
+		Name(state.Name.ValueString()).
+		XStackId(d.config.StackID)
+
+	pjs, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 projects",
+			"Could not read k6 projects with name '"+state.Name.ValueString()+"': "+err.Error(),
+		)
+		return
+	}
+
+	// Process the results and populate the state with the retrieved projects
+	var projectStates []projectDataSourceModel
+	sort.Slice(pjs.Value, func(i, j int) bool {
+		return pjs.Value[i].GetCreated().Before(pjs.Value[j].GetCreated())
+	})
+	for _, pj := range pjs.Value {
+		pjState := projectDataSourceModel{
+			ID:               types.Int32Value(pj.GetId()),
+			Name:             types.StringValue(pj.GetName()),
+			IsDefault:        types.BoolValue(pj.GetIsDefault()),
+			GrafanaFolderUID: handleGrafanaFolderUID(pj.GrafanaFolderUid),
+			Created:          types.StringValue(pj.GetCreated().Format(time.RFC3339Nano)),
+			Updated:          types.StringValue(pj.GetUpdated().Format(time.RFC3339Nano)),
+		}
+
+		projectStates = append(projectStates, pjState)
+	}
+
+	state.Projects = projectStates
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/resources/k6/resource_project.go b/internal/resources/k6/resource_project.go
new file mode 100644
index 00000000..1ab239a4
--- /dev/null
+++ b/internal/resources/k6/resource_project.go
@@ -0,0 +1,297 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.ResourceWithConfigure   = (*projectResource)(nil)
+	_ resource.ResourceWithImportState = (*projectResource)(nil)
+)
+
+var (
+	resourceProjectName = "grafana_k6_project"
+	resourceProjectID   = common.NewResourceID(common.IntIDField("id"))
+)
+
+func resourceProject() *common.Resource {
+	return common.NewResource(
+		common.CategoryK6,
+		resourceProjectName,
+		resourceProjectID,
+		&projectResource{},
+	).WithLister(k6ListerFunction(listProjects))
+}
+
+// projectResourceModel maps the resource schema data.
+type projectResourceModel struct {
+	ID               types.Int32  `tfsdk:"id"`
+	Name             types.String `tfsdk:"name"`
+	IsDefault        types.Bool   `tfsdk:"is_default"`
+	GrafanaFolderUID types.String `tfsdk:"grafana_folder_uid"`
+	Created          types.String `tfsdk:"created"`
+	Updated          types.String `tfsdk:"updated"`
+}
+
+// projectResource is the resource implementation.
+type projectResource struct {
+	basePluginFrameworkResource
+}
+
+// Metadata returns the resource type name.
+func (r *projectResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = resourceProjectName
+}
+
+// Schema defines the schema for the resource.
+func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Manages a k6 project.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "Numeric identifier of the project.",
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the project.",
+				Required:    true,
+			},
+			"is_default": schema.BoolAttribute{
+				Description: "Use this project as default for running tests when no explicit project identifier is provided.",
+				Computed:    true,
+			},
+			"grafana_folder_uid": schema.StringAttribute{
+				Description: "The Grafana folder uid.",
+				Computed:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "The date when the project was created.",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "The date when the project was last updated.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Create creates the resource and sets the Terraform state on success.
+func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	// Retrieve values from plan
+	var plan projectResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Generate API request body from plan
+	toCreate := k6.NewCreateProjectApiModel(plan.Name.ValueString())
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsCreate(ctx).
+		CreateProjectApiModel(toCreate).
+		XStackId(r.config.StackID)
+
+	// Create new project
+	p, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error creating k6 project",
+			"Could not create k6 project, unexpected error: "+err.Error(),
+		)
+		return
+	}
+
+	// Map response body to schema and populate Computed attribute values
+	plan.ID = types.Int32Value(p.GetId())
+	plan.Name = types.StringValue(p.GetName())
+	plan.IsDefault = types.BoolValue(p.GetIsDefault())
+	plan.GrafanaFolderUID = handleGrafanaFolderUID(p.GrafanaFolderUid)
+	plan.Created = types.StringValue(p.GetCreated().Format(time.RFC3339Nano))
+	plan.Updated = types.StringValue(p.GetUpdated().Format(time.RFC3339Nano))
+
+	// Set state to fully populated data
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Read retrieves the resource information.
+func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	// Get current state
+	var state projectResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	p, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project",
+			"Could not read k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	state.ID = types.Int32Value(p.GetId())
+	state.Name = types.StringValue(p.GetName())
+	state.IsDefault = types.BoolValue(p.GetIsDefault())
+	state.GrafanaFolderUID = handleGrafanaFolderUID(p.GrafanaFolderUid)
+	state.Created = types.StringValue(p.GetCreated().Format(time.RFC3339Nano))
+	state.Updated = types.StringValue(p.GetUpdated().Format(time.RFC3339Nano))
+
+	// Set refreshed state
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	// Retrieve values from plan
+	var plan projectResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Get current state to retrieve the ID
+	var state projectResourceModel
+	diags = req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Generate API request body from plan
+	toUpdate := k6.NewPatchProjectApiModel(plan.Name.ValueString())
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	updateReq := r.client.ProjectsAPI.ProjectsPartialUpdate(ctx, state.ID.ValueInt32()).
+		PatchProjectApiModel(toUpdate).
+		XStackId(r.config.StackID)
+
+	// Update the project
+	_, err := updateReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error updating k6 project",
+			"Could not update k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Update resource state with updated items and timestamp
+	fetchReq := r.client.ProjectsAPI.ProjectsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	p, _, err := fetchReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project",
+			"Could not read k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	plan.ID = types.Int32Value(p.GetId())
+	plan.Name = types.StringValue(p.GetName())
+	plan.IsDefault = types.BoolValue(p.GetIsDefault())
+	plan.GrafanaFolderUID = handleGrafanaFolderUID(p.GrafanaFolderUid)
+	plan.Created = types.StringValue(p.GetCreated().Format(time.RFC3339Nano))
+	plan.Updated = types.StringValue(p.GetUpdated().Format(time.RFC3339Nano))
+
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	// Retrieve values from state
+	var state projectResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Delete existing project
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	deleteReq := r.client.ProjectsAPI.ProjectsDestroy(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	_, err := deleteReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error deleting k6 project",
+			"Could not delete k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+	}
+}
+
+func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	id, err := strconv.ParseInt(req.ID, 10, 32)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error importing k6 project",
+			"Could not parse k6 project id "+req.ID+": "+err.Error(),
+		)
+		return
+	}
+
+	resp.State.SetAttribute(ctx, path.Root("id"), types.Int32Value(int32(id)))
+
+	readReq := resource.ReadRequest{State: resp.State}
+	readResp := resource.ReadResponse{State: resp.State}
+
+	r.Read(ctx, readReq, &readResp)
+	resp.Diagnostics.Append(readResp.Diagnostics...)
+}
+
+// listProjects retrieves the list ids of all the existing projects.
+func listProjects(ctx context.Context, client *k6.APIClient, config *k6providerapi.K6APIConfig) ([]string, error) {
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, config.Token)
+	resp, _, err := client.ProjectsAPI.ProjectsList(ctx).
+		XStackId(config.StackID).
+		Execute()
+	if err != nil {
+		return nil, err
+	}
+
+	var ids []string
+	for _, p := range resp.Value {
+		ids = append(ids, strconv.Itoa(int(p.GetId())))
+	}
+	return ids, nil
+}
diff --git a/internal/resources/k6/resource_project_limits.go b/internal/resources/k6/resource_project_limits.go
new file mode 100644
index 00000000..2d614c23
--- /dev/null
+++ b/internal/resources/k6/resource_project_limits.go
@@ -0,0 +1,275 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.ResourceWithConfigure   = (*projectLimitsResource)(nil)
+	_ resource.ResourceWithImportState = (*projectLimitsResource)(nil)
+)
+
+var (
+	resourceProjectLimitsName = "grafana_k6_project_limits"
+	resourceProjectLimitsID   = common.NewResourceID(common.IntIDField("project_id"))
+)
+
+func resourceProjectLimits() *common.Resource {
+	return common.NewResource(
+		common.CategoryK6,
+		resourceProjectLimitsName,
+		resourceProjectLimitsID,
+		&projectLimitsResource{},
+	)
+}
+
+// projectLimitsResourceModel maps the resource schema data.
+type projectLimitsResourceModel struct {
+	ID                  types.Int32 `tfsdk:"id"`
+	ProjectID           types.Int32 `tfsdk:"project_id"`
+	VuhMaxPerMonth      types.Int32 `tfsdk:"vuh_max_per_month"`
+	VuMaxPerTest        types.Int32 `tfsdk:"vu_max_per_test"`
+	VuBrowserMaxPerTest types.Int32 `tfsdk:"vu_browser_max_per_test"`
+	DurationMaxPerTest  types.Int32 `tfsdk:"duration_max_per_test"`
+}
+
+// projectLimitsResource is the resource implementation.
+type projectLimitsResource struct {
+	basePluginFrameworkResource
+}
+
+// Metadata returns the resource type name.
+func (r *projectLimitsResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = resourceProjectLimitsName
+}
+
+// Schema defines the schema for the resource.
+func (r *projectLimitsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Manages limits for a k6 project.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "The identifier of the project limits. This is the same as the project_id.",
+				Computed:    true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project to manage limits for.",
+				Required:    true,
+			},
+			"vuh_max_per_month": schema.Int32Attribute{
+				Description: "Maximum amount of virtual user hours (VU/h) used per one calendar month.",
+				Optional:    true,
+			},
+			"vu_max_per_test": schema.Int32Attribute{
+				Description: "Maximum number of concurrent virtual users (VUs) used in one test.",
+				Optional:    true,
+			},
+			"vu_browser_max_per_test": schema.Int32Attribute{
+				Description: "Maximum number of concurrent browser virtual users (VUs) used in one test.",
+				Optional:    true,
+			},
+			"duration_max_per_test": schema.Int32Attribute{
+				Description: "Maximum duration of a test in seconds.",
+				Optional:    true,
+			},
+		},
+	}
+}
+
+// Create creates the resource and sets the Terraform state on success.
+func (r *projectLimitsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	// Retrieve values from plan
+	var plan projectLimitsResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the project_id
+	plan.ID = plan.ProjectID
+
+	// Generate API request body from plan
+	toUpdate := k6.NewPatchProjectLimitsRequest()
+	if !plan.VuhMaxPerMonth.IsNull() {
+		toUpdate.SetVuhMaxPerMonth(plan.VuhMaxPerMonth.ValueInt32())
+	}
+	if !plan.VuMaxPerTest.IsNull() {
+		toUpdate.SetVuMaxPerTest(plan.VuMaxPerTest.ValueInt32())
+	}
+	if !plan.VuBrowserMaxPerTest.IsNull() {
+		toUpdate.SetVuBrowserMaxPerTest(plan.VuBrowserMaxPerTest.ValueInt32())
+	}
+	if !plan.DurationMaxPerTest.IsNull() {
+		toUpdate.SetDurationMaxPerTest(plan.DurationMaxPerTest.ValueInt32())
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsLimitsPartialUpdate(ctx, plan.ProjectID.ValueInt32()).
+		PatchProjectLimitsRequest(toUpdate).
+		XStackId(r.config.StackID)
+
+	// Update project limits
+	_, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error updating k6 project limits",
+			"Could not update k6 project limits, unexpected error: "+err.Error(),
+		)
+		return
+	}
+
+	// Set state to fully populated data
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Read retrieves the resource information.
+func (r *projectLimitsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	// Get current state
+	var state projectLimitsResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the project_id
+	state.ID = state.ProjectID
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsLimitsRetrieve(ctx, state.ProjectID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	limits, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project limits",
+			"Could not read k6 project limits for project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	state.VuhMaxPerMonth = types.Int32Value(limits.GetVuhMaxPerMonth())
+	state.VuMaxPerTest = types.Int32Value(limits.GetVuMaxPerTest())
+	state.VuBrowserMaxPerTest = types.Int32Value(limits.GetVuBrowserMaxPerTest())
+	state.DurationMaxPerTest = types.Int32Value(limits.GetDurationMaxPerTest())
+
+	// Set refreshed state
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *projectLimitsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	// Retrieve values from plan
+	var plan projectLimitsResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the project_id
+	plan.ID = plan.ProjectID
+
+	// Generate API request body from plan
+	toUpdate := k6.NewPatchProjectLimitsRequest()
+	if !plan.VuhMaxPerMonth.IsNull() {
+		toUpdate.SetVuhMaxPerMonth(plan.VuhMaxPerMonth.ValueInt32())
+	}
+	if !plan.VuMaxPerTest.IsNull() {
+		toUpdate.SetVuMaxPerTest(plan.VuMaxPerTest.ValueInt32())
+	}
+	if !plan.VuBrowserMaxPerTest.IsNull() {
+		toUpdate.SetVuBrowserMaxPerTest(plan.VuBrowserMaxPerTest.ValueInt32())
+	}
+	if !plan.DurationMaxPerTest.IsNull() {
+		toUpdate.SetDurationMaxPerTest(plan.DurationMaxPerTest.ValueInt32())
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsLimitsPartialUpdate(ctx, plan.ProjectID.ValueInt32()).
+		PatchProjectLimitsRequest(toUpdate).
+		XStackId(r.config.StackID)
+
+	// Update project limits
+	_, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error updating k6 project limits",
+			"Could not update k6 project limits for project with id "+strconv.Itoa(int(plan.ProjectID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Set state to fully populated data
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *projectLimitsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	// Retrieve values from state
+	var state projectLimitsResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Reset limits to default values
+	toReset := k6.NewPatchProjectLimitsRequestWithDefaults()
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsLimitsPartialUpdate(ctx, state.ProjectID.ValueInt32()).
+		PatchProjectLimitsRequest(toReset).
+		XStackId(r.config.StackID)
+
+	_, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error resetting k6 project limits",
+			"Could not reset k6 project limits for project with id "+strconv.Itoa(int(state.ProjectID.ValueInt32()))+": "+err.Error(),
+		)
+	}
+}
+
+func (r *projectLimitsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	id, err := strconv.ParseInt(req.ID, 10, 32)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error importing k6 project limits",
+			"Could not parse k6 project id "+req.ID+": "+err.Error(),
+		)
+		return
+	}
+
+	resp.State.SetAttribute(ctx, path.Root("project_id"), types.Int32Value(int32(id)))
+
+	readReq := resource.ReadRequest{State: resp.State}
+	readResp := resource.ReadResponse{State: resp.State}
+
+	r.Read(ctx, readReq, &readResp)
+	resp.Diagnostics.Append(readResp.Diagnostics...)
+}
diff --git a/internal/resources/k6/resources.go b/internal/resources/k6/resources.go
new file mode 100644
index 00000000..32bf6905
--- /dev/null
+++ b/internal/resources/k6/resources.go
@@ -0,0 +1,92 @@
+package k6
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+var Resources = addValidationToResources(
+	resourceProject(),
+	resourceProjectLimits(),
+	resourceLoadTest(),
+)
+
+var DataSources = addValidationToDataSources(
+	dataSourceProject(),
+	dataSourceProjects(),
+	dataSourceProjectLimits(),
+	dataSourceLoadTest(),
+	dataSourceLoadTests(),
+)
+
+func addValidationToResources(resources ...*common.Resource) []*common.Resource {
+	for _, r := range resources {
+		addValidationToSchema(r.Schema)
+	}
+	return resources
+}
+
+func addValidationToDataSources(dataSources ...*common.DataSource) []*common.DataSource {
+	for _, d := range dataSources {
+		addValidationToSchema(d.Schema)
+	}
+	return dataSources
+}
+
+func addValidationToSchema(r *schema.Resource) {
+	if r == nil {
+		return
+	}
+	createFn := r.CreateContext
+	readFn := r.ReadContext
+	updateFn := r.UpdateContext
+	deleteFn := r.DeleteContext
+
+	if createFn != nil {
+		r.CreateContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+			if err := k6ClientResourceValidation(d, m); err != nil {
+				return diag.FromErr(err)
+			}
+			return createFn(ctx, d, m)
+		}
+	}
+
+	if readFn != nil {
+		r.ReadContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+			if err := k6ClientResourceValidation(d, m); err != nil {
+				return diag.FromErr(err)
+			}
+			return readFn(ctx, d, m)
+		}
+	}
+
+	if updateFn != nil {
+		r.UpdateContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+			if err := k6ClientResourceValidation(d, m); err != nil {
+				return diag.FromErr(err)
+			}
+			return updateFn(ctx, d, m)
+		}
+	}
+
+	if deleteFn != nil {
+		r.DeleteContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+			if err := k6ClientResourceValidation(d, m); err != nil {
+				return diag.FromErr(err)
+			}
+			return deleteFn(ctx, d, m)
+		}
+	}
+}
+
+func k6ClientResourceValidation(_ *schema.ResourceData, m interface{}) error {
+	if m.(*common.Client).K6APIClient == nil || m.(*common.Client).K6APIConfig == nil {
+		return fmt.Errorf("the k6 Cloud API client is required for this resource. Set the k6_access_token and stack_id provider attributes")
+	}
+	return nil
+}
diff --git a/internal/resources/machinelearning/resource_job.go b/internal/resources/machinelearning/resource_job.go
index ab184ca5..46b533d1 100644
--- a/internal/resources/machinelearning/resource_job.go
+++ b/internal/resources/machinelearning/resource_job.go
@@ -57,7 +57,7 @@ on available hyperparameters for use in the ` + "`hyper_params`" + ` field.`,
 				Required:    true,
 			},
 			"datasource_type": {
-				Description: "The type of datasource being queried. Currently allowed values are prometheus, graphite, loki, postgres, influxdb, snowflake, splunk, elasticsearch, bigquery, mongodb, and datadog.",
+				Description: "The type of datasource being queried. Currently allowed values are prometheus, grafana-prometheus-datasource, grafana-amazonprometheus-datasource, loki, grafana-loki-datasource, graphite, grafana-graphite-datasource, grafana-datadog-datasource, postgres, grafana-postgresql-datasource, doitintl-bigquery-datasource, grafana-bigquery-datasource, grafana-snowflake-datasource, influxdb, grafana-influxdb-datasource, grafana-splunk-datasource, elasticsearch, grafana-elasticsearch-datasource, and grafana-mongodb-datasource.",
 				Type:        schema.TypeString,
 				Required:    true,
 			},
diff --git a/internal/resources/machinelearning/resource_outlier_detector.go b/internal/resources/machinelearning/resource_outlier_detector.go
index 55b8b848..047312c2 100644
--- a/internal/resources/machinelearning/resource_outlier_detector.go
+++ b/internal/resources/machinelearning/resource_outlier_detector.go
@@ -60,10 +60,22 @@ Visit https://grafana.com/docs/grafana-cloud/machine-learning/outlier-detection/
 				Required:    true,
 			},
 			"datasource_type": {
-				Description:  "The type of datasource being queried. Currently allowed values are prometheus, graphite, loki, postgres, influxdb, snowflake, splunk, elasticsearch, bigquery, mongodb, and datadog.",
-				Type:         schema.TypeString,
-				Required:     true,
-				ValidateFunc: validation.StringInSlice([]string{"prometheus", "graphite", "loki", "postgres", "influxdb", "snowflake", "splunk", "elasticsearch", "bigquery", "mongodb", "datadog"}, false),
+				Description: "The type of datasource being queried. Currently allowed values are prometheus, grafana-prometheus-datasource, grafana-amazonprometheus-datasource, loki, grafana-loki-datasource, graphite, grafana-graphite-datasource, grafana-datadog-datasource, postgres, grafana-postgresql-datasource, doitintl-bigquery-datasource, grafana-bigquery-datasource, grafana-snowflake-datasource, influxdb, grafana-influxdb-datasource, grafana-splunk-datasource, elasticsearch, grafana-elasticsearch-datasource, and grafana-mongodb-datasource.",
+				Type:        schema.TypeString,
+				Required:    true,
+				ValidateFunc: validation.StringInSlice([]string{
+					"prometheus", "grafana-prometheus-datasource", "grafana-amazonprometheus-datasource",
+					"loki", "grafana-loki-datasource",
+					"graphite", "grafana-graphite-datasource",
+					"grafana-datadog-datasource",
+					"postgres", "grafana-postgresql-datasource",
+					"doitintl-bigquery-datasource", "grafana-bigquery-datasource",
+					"grafana-snowflake-datasource",
+					"influxdb", "grafana-influxdb-datasource",
+					"grafana-splunk-datasource",
+					"elasticsearch", "grafana-elasticsearch-datasource",
+					"grafana-mongodb-datasource",
+				}, false),
 			},
 			"query_params": {
 				Description: "An object representing the query params to query Grafana with.",
diff --git a/pkg/generate/postprocessing/replace_references.go b/pkg/generate/postprocessing/replace_references.go
index 13c558da..ccbe5fce 100644
--- a/pkg/generate/postprocessing/replace_references.go
+++ b/pkg/generate/postprocessing/replace_references.go
@@ -70,6 +70,12 @@ var knownReferences = []string{
 	"grafana_folder_permission_item.team=grafana_team.id",
 	"grafana_folder_permission_item.user=grafana_service_account.id",
 	"grafana_folder_permission_item.user=grafana_user.id",
+	"grafana_k6_installation.cloud_access_policy_token=grafana_cloud_access_policy_token.token",
+	"grafana_k6_installation.grafana_sa_token=grafana_cloud_stack_service_account_token.key",
+	"grafana_k6_installation.k6_access_token=grafana_k6_installation.k6_access_token",
+	"grafana_k6_installation.stack_id=grafana_cloud_stack.id",
+	"grafana_k6_load_test.project_id=grafana_k6_project.id",
+	"grafana_k6_project_limits.project_id=grafana_k6_project.id",
 	"grafana_library_panel.folder_uid=grafana_folder.uid",
 	"grafana_library_panel.org_id=grafana_organization.id",
 	"grafana_machine_learning_alert.job_id=grafana_machine_learning_job.id",
diff --git a/pkg/provider/configure_clients.go b/pkg/provider/configure_clients.go
index 6a4a5ed8..e86905eb 100644
--- a/pkg/provider/configure_clients.go
+++ b/pkg/provider/configure_clients.go
@@ -17,9 +17,11 @@ import (
 	"github.com/grafana/grafana-app-sdk/resource"
 	"github.com/grafana/grafana-com-public-clients/go/gcom"
 	goapi "github.com/grafana/grafana-openapi-client-go/client"
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
 	"github.com/grafana/machine-learning-go-client/mlapi"
 	"github.com/grafana/slo-openapi-client/go/slo"
 	SMAPI "github.com/grafana/synthetic-monitoring-api-go-client"
+
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/rest"
 
@@ -33,6 +35,7 @@ import (
 	"github.com/grafana/terraform-provider-grafana/v3/internal/common/connectionsapi"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/common/fleetmanagementapi"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/common/frontendo11yapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/grafana"
 )
 
@@ -92,6 +95,12 @@ func CreateClients(providerConfig ProviderConfig) (*common.Client, error) {
 		}
 	}
 
+	if !providerConfig.K6AccessToken.IsNull() && !providerConfig.StackID.IsNull() {
+		if err := createK6Client(c, providerConfig); err != nil {
+			return nil, err
+		}
+	}
+
 	grafana.StoreDashboardSHA256 = providerConfig.StoreDashboardSha256.ValueBool()
 
 	return c, nil
@@ -357,6 +366,37 @@ func createConnectionsClient(client *common.Client, providerConfig ProviderConfi
 	return nil
 }
 
+func createK6Client(client *common.Client, providerConfig ProviderConfig) error {
+	k6Cfg := k6.NewConfiguration()
+	if !providerConfig.K6URL.IsNull() {
+		k6Cfg.Servers = []k6.ServerConfiguration{
+			{URL: providerConfig.K6URL.ValueString()},
+		}
+	}
+
+	k6Cfg.HTTPClient = getRetryClient(providerConfig)
+
+	httpHeaders, err := getHTTPHeadersMap(providerConfig)
+	if err != nil {
+		return err
+	}
+	for k, v := range httpHeaders {
+		k6Cfg.DefaultHeader[k] = v
+	}
+
+	var stackID int32
+	if stackID, err = common.ToInt32(providerConfig.StackID.ValueInt64()); err != nil {
+		return fmt.Errorf("could not convert stack_id to int32: %s", err.Error())
+	}
+
+	client.K6APIClient = k6.NewAPIClient(k6Cfg)
+	client.K6APIConfig = &k6providerapi.K6APIConfig{
+		Token:   providerConfig.K6AccessToken.ValueString(),
+		StackID: stackID,
+	}
+	return nil
+}
+
 func createFleetManagementClient(client *common.Client, providerConfig ProviderConfig) error {
 	providerHeaders, err := getHTTPHeadersMap(providerConfig)
 	if err != nil {
diff --git a/pkg/provider/framework_provider.go b/pkg/provider/framework_provider.go
index 9a093878..06faef10 100644
--- a/pkg/provider/framework_provider.go
+++ b/pkg/provider/framework_provider.go
@@ -53,6 +53,9 @@ type ProviderConfig struct {
 	FleetManagementURL         types.String `tfsdk:"fleet_management_url"`
 	FrontendO11yAPIAccessToken types.String `tfsdk:"frontend_o11y_api_access_token"`
 
+	K6URL         types.String `tfsdk:"k6_url"`
+	K6AccessToken types.String `tfsdk:"k6_access_token"`
+
 	UserAgent types.String `tfsdk:"-"`
 	Version   types.String `tfsdk:"-"`
 }
@@ -91,6 +94,9 @@ func (c *ProviderConfig) SetDefaults() error {
 		c.StackID = v
 	}
 
+	c.K6URL = envDefaultFuncString(c.K6URL, "GRAFANA_K6_URL")
+	c.K6AccessToken = envDefaultFuncString(c.K6AccessToken, "GRAFANA_K6_ACCESS_TOKEN")
+
 	if c.StoreDashboardSha256, err = envDefaultFuncBool(c.StoreDashboardSha256, "GRAFANA_STORE_DASHBOARD_SHA256", false); err != nil {
 		return fmt.Errorf("failed to parse GRAFANA_STORE_DASHBOARD_SHA256: %w", err)
 	}
@@ -261,6 +267,15 @@ func (p *frameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest,
 				Sensitive:           true,
 				MarkdownDescription: "A Grafana Frontend Observability API access token. May alternatively be set via the `GRAFANA_FRONTEND_O11Y_API_ACCESS_TOKEN` environment variable.",
 			},
+			"k6_url": schema.StringAttribute{
+				Optional:            true,
+				MarkdownDescription: "The k6 Cloud API url. May alternatively be set via the `GRAFANA_K6_URL` environment variable.",
+			},
+			"k6_access_token": schema.StringAttribute{
+				Optional:            true,
+				Sensitive:           true,
+				MarkdownDescription: "The k6 Cloud API token. May alternatively be set via the `GRAFANA_K6_ACCESS_TOKEN` environment variable.",
+			},
 		},
 	}
 }
diff --git a/pkg/provider/legacy_provider.go b/pkg/provider/legacy_provider.go
index 6bee0165..181221a4 100644
--- a/pkg/provider/legacy_provider.go
+++ b/pkg/provider/legacy_provider.go
@@ -194,6 +194,19 @@ func Provider(version string) *schema.Provider {
 				Sensitive:   true,
 				Description: "A Grafana Frontend Observability API access token. May alternatively be set via the `GRAFANA_FRONTEND_O11Y_API_ACCESS_TOKEN` environment variable.",
 			},
+
+			"k6_url": {
+				Type:         schema.TypeString,
+				Optional:     true,
+				Description:  "The k6 Cloud API url. May alternatively be set via the `GRAFANA_K6_URL` environment variable.",
+				ValidateFunc: validation.IsURLWithHTTPorHTTPS,
+			},
+			"k6_access_token": {
+				Type:        schema.TypeString,
+				Optional:    true,
+				Sensitive:   true,
+				Description: "The k6 Cloud API token. May alternatively be set via the `GRAFANA_K6_ACCESS_TOKEN` environment variable.",
+			},
 		},
 
 		ResourcesMap:   legacySDKResources(),
@@ -271,6 +284,8 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema
 			FleetManagementAuth:        stringValueOrNull(d, "fleet_management_auth"),
 			FleetManagementURL:         stringValueOrNull(d, "fleet_management_url"),
 			FrontendO11yAPIAccessToken: stringValueOrNull(d, "frontend_o11y_api_access_token"),
+			K6URL:                      stringValueOrNull(d, "k6_url"),
+			K6AccessToken:              stringValueOrNull(d, "k6_access_token"),
 			StoreDashboardSha256:       boolValueOrNull(d, "store_dashboard_sha256"),
 			HTTPHeaders:                headers,
 			Retries:                    int64ValueOrNull(d, "retries"),
diff --git a/pkg/provider/resources.go b/pkg/provider/resources.go
index 586e6a31..a3613210 100644
--- a/pkg/provider/resources.go
+++ b/pkg/provider/resources.go
@@ -11,6 +11,7 @@ import (
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/fleetmanagement"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/frontendo11y"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/grafana"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/k6"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/machinelearning"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/oncall"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/slo"
@@ -27,6 +28,7 @@ func DataSources() []*common.DataSource {
 	resources = append(resources, machinelearning.DataSources...)
 	resources = append(resources, oncall.DataSources...)
 	resources = append(resources, slo.DataSources...)
+	resources = append(resources, k6.DataSources...)
 	resources = append(resources, syntheticmonitoring.DataSources...)
 	resources = append(resources, cloudprovider.DataSources...)
 	resources = append(resources, connections.DataSources...)
@@ -66,6 +68,7 @@ func Resources() []*common.Resource {
 	resources = append(resources, machinelearning.Resources...)
 	resources = append(resources, oncall.Resources...)
 	resources = append(resources, slo.Resources...)
+	resources = append(resources, k6.Resources...)
 	resources = append(resources, syntheticmonitoring.Resources...)
 	resources = append(resources, cloudprovider.Resources...)
 	resources = append(resources, connections.Resources...)



Test patch:
diff --git a/examples/data-sources/grafana_k6_load_test/data-source.tf b/examples/data-sources/grafana_k6_load_test/data-source.tf
new file mode 100644
index 00000000..d9d1dc27
--- /dev/null
+++ b/examples/data-sources/grafana_k6_load_test/data-source.tf
@@ -0,0 +1,17 @@
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+}
+
+data "grafana_k6_load_test" "from_id" {
+  id = grafana_k6_load_test.test_load_test.id
+}
diff --git a/examples/data-sources/grafana_k6_load_tests/data-source.tf b/examples/data-sources/grafana_k6_load_tests/data-source.tf
new file mode 100644
index 00000000..c659dd34
--- /dev/null
+++ b/examples/data-sources/grafana_k6_load_tests/data-source.tf
@@ -0,0 +1,49 @@
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+
+  depends_on = [
+    grafana_k6_project.load_test_project,
+  ]
+}
+
+resource "grafana_k6_load_test" "test_load_test_2" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test (2)"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test,
+  ]
+}
+
+data "grafana_k6_load_tests" "from_project_id" {
+  project_id = grafana_k6_project.load_test_project.id
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test,
+    grafana_k6_load_test.test_load_test_2
+  ]
+}
+
+data "grafana_k6_load_tests" "filter_by_name" {
+  name       = "Terraform Test Load Test (2)"
+  project_id = grafana_k6_project.load_test_project.id
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test_2,
+  ]
+}
\ No newline at end of file
diff --git a/examples/resources/grafana_k6_load_test/import.sh b/examples/resources/grafana_k6_load_test/import.sh
new file mode 100644
index 00000000..4c8c2bcd
--- /dev/null
+++ b/examples/resources/grafana_k6_load_test/import.sh
@@ -0,0 +1 @@
+terraform import grafana_k6_load_test.name "{{ id }}"
diff --git a/examples/resources/grafana_k6_load_test/resource.tf b/examples/resources/grafana_k6_load_test/resource.tf
new file mode 100644
index 00000000..749c8406
--- /dev/null
+++ b/examples/resources/grafana_k6_load_test/resource.tf
@@ -0,0 +1,13 @@
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+}
\ No newline at end of file
diff --git a/internal/resources/cloud/resource_k6_installation_test.go b/internal/resources/cloud/resource_k6_installation_test.go
new file mode 100644
index 00000000..554b1238
--- /dev/null
+++ b/internal/resources/cloud/resource_k6_installation_test.go
@@ -0,0 +1,71 @@
+package cloud_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/grafana-com-public-clients/go/gcom"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccK6Installation(t *testing.T) {
+	testutils.CheckCloudAPITestsEnabled(t)
+
+	var stack gcom.FormattedApiInstance
+	stackPrefix := "tfk6installtest"
+	testAccDeleteExistingStacks(t, stackPrefix)
+	stackSlug := GetRandomStackName(stackPrefix)
+
+	accessPolicyPrefix := "testk6install-"
+	testAccDeleteExistingAccessPolicies(t, "us", accessPolicyPrefix)
+	accessPolicyName := accessPolicyPrefix + acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy:             testAccStackCheckDestroy(&stack),
+		Steps: []resource.TestStep{
+			{
+				Config: testAccK6Installation(stackSlug, accessPolicyName),
+				Check: resource.ComposeTestCheckFunc(
+					testAccStackCheckExists("grafana_cloud_stack.test", &stack),
+					resource.TestCheckResourceAttrSet("grafana_k6_installation.test", "k6_access_token"),
+					resource.TestCheckResourceAttrSet("grafana_k6_installation.test", "k6_organization"),
+				),
+			},
+		},
+	})
+}
+
+func testAccK6InstallationBase(stackSlug, accessPolicyName string) string {
+	return testAccStackConfigBasic(stackSlug, stackSlug, "description") +
+		testAccCloudAccessPolicyTokenConfigBasic(accessPolicyName, accessPolicyName, "eu", []string{"stacks:read", "stacks:write", "subscriptions:read", "orgs:read"}, "")
+}
+
+func testAccK6Installation(stackSlug, apiKeyName string) string {
+	return testAccK6InstallationBase(stackSlug, apiKeyName) +
+		`
+	resource "grafana_cloud_stack_service_account" "tfk6installtest_sa" {
+		stack_slug = grafana_cloud_stack.test.slug
+		name        = "tfk6installtest-sa"
+		role        = "Admin"
+		is_disabled = false 
+	}
+
+	resource "grafana_cloud_stack_service_account_token" "tfk6installtest_sa_token" {
+		stack_slug = grafana_cloud_stack.test.slug
+		service_account_id = grafana_cloud_stack_service_account.tfk6installtest_sa.id
+		name       = "tfk6installtest-sa-token"
+	}
+	` +
+		`
+	resource "grafana_k6_installation" "test" {
+		cloud_access_policy_token = grafana_cloud_access_policy_token.test.token
+		stack_id                  = grafana_cloud_stack.test.id
+		grafana_sa_token          = grafana_cloud_stack_service_account_token.tfk6installtest_sa_token.key
+		grafana_user              = "admin"
+	}
+	`
+}
diff --git a/internal/resources/examples_test.go b/internal/resources/examples_test.go
index 5411775b..a0c5aa5d 100644
--- a/internal/resources/examples_test.go
+++ b/internal/resources/examples_test.go
@@ -103,6 +103,13 @@ func TestAccExamples(t *testing.T) {
 				testutils.CheckCloudInstanceTestsEnabled(t)
 			},
 		},
+		{
+			category: "k6",
+			testCheck: func(t *testing.T, filename string) {
+				t.Skip() // TODO: Make all examples work
+				testutils.CheckCloudInstanceTestsEnabled(t)
+			},
+		},
 		{
 			category: "Cloud Provider",
 			testCheck: func(t *testing.T, filename string) {
diff --git a/internal/resources/grafana/resource_alerting_contact_point_test.go b/internal/resources/grafana/resource_alerting_contact_point_test.go
index 5ebda694..2f46f0aa 100644
--- a/internal/resources/grafana/resource_alerting_contact_point_test.go
+++ b/internal/resources/grafana/resource_alerting_contact_point_test.go
@@ -304,6 +304,10 @@ func TestAccContactPoint_notifiers(t *testing.T) {
 					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "webhook.0.max_alerts", "100"),
 					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "webhook.0.message", "Custom message"),
 					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "webhook.0.title", "Custom title"),
+					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "webhook.0.tls_config.insecure_skip_verify", "true"),
+					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "webhook.0.tls_config.ca_certificate", "ca.crt"),
+					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "webhook.0.tls_config.client_certificate", "client.crt"),
+					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "webhook.0.tls_config.client_key", "client.key"),
 					// wecom
 					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "wecom.#", "1"),
 					resource.TestCheckResourceAttr("grafana_contact_point.receiver_types", "wecom.0.url", "http://wecom-url"),
diff --git a/internal/resources/grafana/resource_alerting_rule_group_test.go b/internal/resources/grafana/resource_alerting_rule_group_test.go
index 448571fa..48b5c48f 100644
--- a/internal/resources/grafana/resource_alerting_rule_group_test.go
+++ b/internal/resources/grafana/resource_alerting_rule_group_test.go
@@ -723,6 +723,45 @@ func TestAccAlertRule_zeroSeconds(t *testing.T) {
 	})
 }
 
+func TestAccAlertRule_keepFiringFor(t *testing.T) {
+	testutils.CheckOSSTestsEnabled(t, ">=11.6.0")
+
+	var group models.AlertRuleGroup
+	var name = acctest.RandString(10)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy:             alertingRuleGroupCheckExists.destroyed(&group, nil),
+		Steps: []resource.TestStep{
+			{
+				Config: testAccAlertRuleKeepFiringFor(name, "5m"),
+				Check: resource.ComposeTestCheckFunc(
+					alertingRuleGroupCheckExists.exists("grafana_rule_group.my_rule_group", &group),
+					resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "name", name),
+					resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.#", "1"),
+					resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.name", "My Keep Firing Test"),
+					resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.keep_firing_for", "5m0s"),
+				),
+			},
+			{
+				Config: testAccAlertRuleKeepFiringFor(name, "10m"),
+				Check: resource.ComposeTestCheckFunc(
+					alertingRuleGroupCheckExists.exists("grafana_rule_group.my_rule_group", &group),
+					resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "name", name),
+					resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.#", "1"),
+					resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.name", "My Keep Firing Test"),
+					resource.TestCheckResourceAttr("grafana_rule_group.my_rule_group", "rule.0.keep_firing_for", "10m0s"),
+				),
+			},
+			{
+				ResourceName:      "grafana_rule_group.my_rule_group",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
 func TestAccAlertRule_NotificationSettings(t *testing.T) {
 	testutils.CheckOSSTestsEnabled(t, ">=10.4.0")
 
@@ -1044,3 +1083,136 @@ resource "grafana_rule_group" "my_rule_group" {
 	}
 }`, name, metric, refID)
 }
+
+func testAccAlertRuleKeepFiringFor(name string, keepFiringFor string) string {
+	return fmt.Sprintf(`
+resource "grafana_folder" "rule_folder" {
+	title = "%[1]s"
+}
+
+resource "grafana_data_source" "testdata_datasource" {
+	name = "%[1]s"
+	type = "grafana-testdata-datasource"
+	url  = "http://localhost:3333"
+}
+
+resource "grafana_rule_group" "my_rule_group" {
+	name             = "%[1]s"
+	folder_uid       = grafana_folder.rule_folder.uid
+	interval_seconds = 60
+	org_id           = 1
+
+	rule {
+		name           = "My Keep Firing Test"
+		for            = "1m"
+		keep_firing_for = "%[2]s"
+		condition      = "C"
+		no_data_state  = "NoData"
+		exec_err_state = "Alerting"
+		is_paused = false
+		
+		// Query the datasource.
+		data {
+			ref_id = "A"
+			relative_time_range {
+				from = 600
+				to   = 0
+			}
+			datasource_uid = grafana_data_source.testdata_datasource.uid
+			model = jsonencode({
+				intervalMs    = 1000
+				maxDataPoints = 43200
+				refId         = "A"
+			})
+		}
+		
+		data {
+			ref_id     = "B"
+			query_type = ""
+			relative_time_range {
+				from = 0
+				to   = 0
+			}
+			datasource_uid = "-100"
+			model = jsonencode({
+				conditions = [
+					{
+						evaluator = {
+							params = [
+								3
+							],
+							type = "gt"
+						},
+						operator = {
+							type = "and"
+						},
+						query = {
+							params = [
+								"A"
+							]
+						},
+						reducer = {
+							params = [],
+							type = "last"
+						},
+						type = "query"
+					}
+				],
+				datasource = {
+					type = "__expr__",
+					uid = "-100"
+				},
+				hide = false,
+				intervalMs = 1000,
+				maxDataPoints = 43200,
+				refId = "B",
+				type = "classic_conditions"
+			})
+		}
+		
+		data {
+			ref_id     = "C"
+			query_type = ""
+			relative_time_range {
+				from = 0
+				to   = 0
+			}
+			datasource_uid = "-100"
+			model = jsonencode({
+				conditions = [
+					{
+						evaluator = {
+							params = [
+								0, 0
+							],
+							type = "within_range"
+						},
+						operator = {
+							type = "and"
+						},
+						query = {
+							params = [
+								"B"
+							]
+						},
+						reducer = {
+							params = [],
+							type = "last"
+						},
+						type = "query"
+					}
+				],
+				datasource = {
+					type = "__expr__",
+					uid = "-100"
+				},
+				hide = false,
+				intervalMs = 1000,
+				maxDataPoints = 43200,
+				refId = "C",
+				type = "classic_conditions"
+			})
+		}
+	}
+}`, name, keepFiringFor)
+}
diff --git a/internal/resources/k6/common_check_exists_test.go b/internal/resources/k6/common_check_exists_test.go
new file mode 100644
index 00000000..0b23edc8
--- /dev/null
+++ b/internal/resources/k6/common_check_exists_test.go
@@ -0,0 +1,129 @@
+package k6_test
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+// Helpers that check if a resource exists or doesn't. To define a new one, use the newCheckExistsHelper function.
+// A function that gets a resource by their Terraform id is required.
+var (
+	projectCheckExists = newCheckExistsHelper(
+		func(p *k6.ProjectApiModel) int32 { return p.GetId() },
+		func(client *k6.APIClient, config *k6providerapi.K6APIConfig, id int32) (*k6.ProjectApiModel, error) {
+			ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+			m, _, err := client.ProjectsAPI.ProjectsRetrieve(ctx, id).
+				XStackId(config.StackID).
+				Execute()
+			return payloadOrError(m, err)
+		},
+	)
+	projectLimitsCheckExists = newCheckExistsHelper(
+		func(p *k6.ProjectLimitsApiModel) int32 { return p.GetProjectId() },
+		func(client *k6.APIClient, config *k6providerapi.K6APIConfig, id int32) (*k6.ProjectLimitsApiModel, error) {
+			ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+			m, _, err := client.ProjectsAPI.ProjectsLimitsRetrieve(ctx, id).
+				XStackId(config.StackID).
+				Execute()
+			return payloadOrError(m, err)
+		},
+	)
+	loadTestCheckExists = newCheckExistsHelper(
+		func(lt *k6.LoadTestApiModel) int32 { return lt.GetId() },
+		func(client *k6.APIClient, config *k6providerapi.K6APIConfig, id int32) (*k6.LoadTestApiModel, error) {
+			ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+			m, _, err := client.LoadTestsAPI.LoadTestsRetrieve(ctx, id).
+				XStackId(config.StackID).
+				Execute()
+			return payloadOrError(m, err)
+		},
+	)
+)
+
+type checkExistsGetResourceFunc[T interface{}] func(client *k6.APIClient, config *k6providerapi.K6APIConfig, id int32) (*T, error)
+type checkExistsGetIDFunc[T interface{}] func(*T) int32
+
+type checkExistsHelper[T interface{}] struct {
+	getIDFunc       func(*T) int32
+	getResourceFunc checkExistsGetResourceFunc[T]
+}
+
+// newCheckExistsHelper creates a test helper that checks if a resource exists or not.
+// The getIDFunc function should return the id of the resource.
+// The getResourceFunc function should return the resource from the given id.
+func newCheckExistsHelper[T interface{}](getIDFunc checkExistsGetIDFunc[T], getResourceFunc checkExistsGetResourceFunc[T]) checkExistsHelper[T] {
+	return checkExistsHelper[T]{getIDFunc: getIDFunc, getResourceFunc: getResourceFunc}
+}
+
+// exists checks that the resource exists.
+func (h *checkExistsHelper[T]) exists(rn string, v *T) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[rn]
+		if !ok {
+			return fmt.Errorf("resource not found: %s\n %#v", rn, s.RootModule().Resources)
+		}
+		if rs.Primary.ID == "" {
+			return fmt.Errorf("resource id not set")
+		}
+
+		var resourceID int32
+		if intResourceID, err := strconv.Atoi(rs.Primary.ID); err != nil {
+			return fmt.Errorf("could not convert resource id to integer: %s", err.Error())
+		} else if resourceID, err = common.ToInt32(intResourceID); err != nil {
+			return fmt.Errorf("could not convert resource id to int32: %s", err.Error())
+		}
+
+		obj, err := h.getResourceFunc(
+			testutils.Provider.Meta().(*common.Client).K6APIClient,
+			testutils.Provider.Meta().(*common.Client).K6APIConfig,
+			resourceID,
+		)
+		if err != nil {
+			return fmt.Errorf("error getting resource %s with id %q: %s", rn, rs.Primary.ID, err)
+		}
+
+		// Sanity check: The "destroyed" function should fail here because the resource exists
+		if err := h.destroyed(obj)(s); err == nil {
+			return fmt.Errorf("the destroyed check passed but shouldn't for resource %s with id %q. This is a bug in the test", rn, rs.Primary.ID)
+		}
+
+		*v = *obj
+		return nil
+	}
+}
+
+// destroyed checks that the resource doesn't exist.
+func (h *checkExistsHelper[T]) destroyed(v *T) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		resourceID := h.getIDFunc(v)
+		_, err := h.getResourceFunc(
+			testutils.Provider.Meta().(*common.Client).K6APIClient,
+			testutils.Provider.Meta().(*common.Client).K6APIConfig,
+			resourceID,
+		)
+		if err == nil {
+			return fmt.Errorf("%T %d still exists", v, resourceID)
+		} else if !common.IsNotFoundError(err) {
+			return fmt.Errorf("error checking if resource with id %d was destroyed: %s", resourceID, err)
+		}
+		return nil
+	}
+}
+
+// payloadOrError returns the error if not nil, or the payload otherwise.
+func payloadOrError[T interface{}](t *T, err error) (*T, error) {
+	if err != nil {
+		return nil, err
+	}
+	return t, nil
+}
diff --git a/internal/resources/k6/data_source_k6_loadtest.go b/internal/resources/k6/data_source_k6_loadtest.go
new file mode 100644
index 00000000..6333827e
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_loadtest.go
@@ -0,0 +1,145 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*loadTestDataSource)(nil)
+)
+
+var (
+	dataSourceLoadTestName = "grafana_k6_load_test"
+)
+
+func dataSourceLoadTest() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceLoadTestName,
+		&loadTestDataSource{},
+	)
+}
+
+// loadTestDataSourceModel maps the data source schema data.
+type loadTestDataSourceModel struct {
+	ID                types.Int32  `tfsdk:"id"`
+	ProjectID         types.Int32  `tfsdk:"project_id"`
+	Name              types.String `tfsdk:"name"`
+	Script            types.String `tfsdk:"script"`
+	BaselineTestRunID types.Int32  `tfsdk:"baseline_test_run_id"`
+	Created           types.String `tfsdk:"created"`
+	Updated           types.String `tfsdk:"updated"`
+}
+
+// loadTestDataSource is the data source implementation.
+type loadTestDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *loadTestDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceLoadTestName
+}
+
+// Schema defines the schema for the data source.
+func (d *loadTestDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves a k6 load test.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "Numeric identifier of the load test.",
+				Required:    true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project this load test belongs to.",
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the load test.",
+				Computed:    true,
+			},
+			"script": schema.StringAttribute{
+				Description: "The k6 test script content.",
+				Computed:    true,
+			},
+			"baseline_test_run_id": schema.Int32Attribute{
+				Description: "Identifier of a baseline test run used for results comparison.",
+				Computed:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "The date when the load test was created.",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "The date when the load test was last updated.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *loadTestDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state loadTestDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Retrieve the load test attributes
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.LoadTestsAPI.LoadTestsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	lt, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test",
+			"Could not read k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Retrieve the load test script content
+	scriptReq := d.client.LoadTestsAPI.LoadTestsScriptRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	script, _, err := scriptReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test script",
+			"Could not read k6 load test script with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	state.Name = types.StringValue(lt.GetName())
+	state.ProjectID = types.Int32Value(lt.GetProjectId())
+	state.BaselineTestRunID = handleBaselineTestRunID(lt.GetBaselineTestRunId())
+	state.Script = types.StringValue(script)
+	state.Created = types.StringValue(lt.GetCreated().Format(time.RFC3339Nano))
+	state.Updated = types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano))
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
+
+func handleBaselineTestRunID(baselineTestRunID int32) types.Int32 {
+	if baselineTestRunID == 0 {
+		// If the API returned 0, set it as null
+		return types.Int32Null()
+	}
+	return types.Int32Value(baselineTestRunID)
+}
diff --git a/internal/resources/k6/data_source_k6_loadtest_test.go b/internal/resources/k6/data_source_k6_loadtest_test.go
new file mode 100644
index 00000000..bedba089
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_loadtest_test.go
@@ -0,0 +1,29 @@
+package k6_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6LoadTest_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_load_test/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_test.from_id", "id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_load_test.from_id", "name", "Terraform Test Load Test"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_test.from_id", "script"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_test.from_id", "created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_test.from_id", "updated"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/data_source_k6_loadtests.go b/internal/resources/k6/data_source_k6_loadtests.go
new file mode 100644
index 00000000..ffc6722b
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_loadtests.go
@@ -0,0 +1,161 @@
+package k6
+
+import (
+	"context"
+	"slices"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*loadTestsDataSource)(nil)
+)
+
+var (
+	dataSourceLoadTestsName = "grafana_k6_load_tests"
+)
+
+func dataSourceLoadTests() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceLoadTestsName,
+		&loadTestsDataSource{},
+	)
+}
+
+// loadTestsDataSourceModel maps the data source schema data.
+type loadTestsDataSourceModel struct {
+	ID        types.Int32               `tfsdk:"id"`
+	ProjectID types.Int32               `tfsdk:"project_id"`
+	Name      types.String              `tfsdk:"name"`
+	LoadTests []loadTestDataSourceModel `tfsdk:"load_tests"`
+}
+
+// loadTestsDataSource is the data source implementation.
+type loadTestsDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *loadTestsDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceLoadTestsName
+}
+
+// Schema defines the schema for the data source.
+func (d *loadTestsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves all k6 load tests that belong to a project.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "The identifier of the project the load tests belong to. " +
+					"This is set to the same as the project_id.",
+				Computed: true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project the load tests belong to.",
+				Required:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the load test.",
+				Optional:    true,
+			},
+			"load_tests": schema.ListAttribute{
+				Computed: true,
+				ElementType: types.ObjectType{
+					AttrTypes: map[string]attr.Type{
+						"id":                   types.Int32Type,
+						"name":                 types.StringType,
+						"project_id":           types.Int32Type,
+						"script":               types.StringType,
+						"baseline_test_run_id": types.Int32Type,
+						"created":              types.StringType,
+						"updated":              types.StringType,
+					},
+				},
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *loadTestsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state loadTestsDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the project_id
+	state.ID = state.ProjectID
+
+	// Retrieve the project's load tests
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.LoadTestsAPI.ProjectsLoadTestsRetrieve(ctx, state.ProjectID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	lts, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load tests",
+			"Could not read k6 load tests with project id "+strconv.Itoa(int(state.ProjectID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Filter the results by name, if provided
+	if !state.Name.IsNull() {
+		lts.Value = slices.DeleteFunc(lts.Value, func(lt k6.LoadTestApiModel) bool {
+			return !strings.EqualFold(lt.GetName(), state.Name.ValueString())
+		})
+	}
+
+	// Process the results and populate the state with the retrieved load tests
+	var loadTests []loadTestDataSourceModel
+	sort.Slice(lts.Value, func(i, j int) bool {
+		return lts.Value[i].GetCreated().Before(lts.Value[j].GetCreated())
+	})
+	for _, lt := range lts.Value {
+		// Retrieve the load test script content
+		scriptReq := d.client.LoadTestsAPI.LoadTestsScriptRetrieve(ctx, lt.GetId()).
+			XStackId(d.config.StackID)
+
+		script, _, err := scriptReq.Execute()
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"Error reading k6 load test script",
+				"Could not read k6 load test script with id"+strconv.Itoa(int(lt.GetId()))+": "+err.Error(),
+			)
+			return
+		}
+
+		ltState := loadTestDataSourceModel{
+			ID:                types.Int32Value(lt.GetId()),
+			Name:              types.StringValue(lt.GetName()),
+			ProjectID:         types.Int32Value(lt.GetProjectId()),
+			BaselineTestRunID: handleBaselineTestRunID(lt.GetBaselineTestRunId()),
+			Script:            types.StringValue(script),
+			Created:           types.StringValue(lt.GetCreated().Format(time.RFC3339Nano)),
+			Updated:           types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano)),
+		}
+
+		loadTests = append(loadTests, ltState)
+	}
+
+	state.LoadTests = loadTests
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/resources/k6/data_source_k6_loadtests_test.go b/internal/resources/k6/data_source_k6_loadtests_test.go
new file mode 100644
index 00000000..bd708da3
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_loadtests_test.go
@@ -0,0 +1,59 @@
+package k6_test
+
+import (
+	"fmt"
+	"strconv"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6LoadTests_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var project k6.ProjectApiModel
+
+	checkProjectIDMatch := func(value string) error {
+		if value != strconv.Itoa(int(project.GetId())) {
+			return fmt.Errorf("project_id does not match the expected value: %s", value)
+		}
+		return nil
+	}
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_load_tests/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.load_test_project", &project),
+					// from_project_id.0
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.0.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_load_tests.from_project_id", "load_tests.0.name", "Terraform Test Load Test"),
+					resource.TestCheckResourceAttrWith("data.grafana_k6_load_tests.from_project_id", "load_tests.0.project_id", checkProjectIDMatch),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.0.script"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.0.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.0.updated"),
+					// from_project_id.1
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.1.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_load_tests.from_project_id", "load_tests.1.name", "Terraform Test Load Test (2)"),
+					resource.TestCheckResourceAttrWith("data.grafana_k6_load_tests.from_project_id", "load_tests.1.project_id", checkProjectIDMatch),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.1.script"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.1.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.1.updated"),
+					// filter_by_name.0
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.name", "Terraform Test Load Test (2)"),
+					resource.TestCheckResourceAttrWith("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.project_id", checkProjectIDMatch),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.script"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.updated"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/data_source_k6_project_limits_test.go b/internal/resources/k6/data_source_k6_project_limits_test.go
new file mode 100644
index 00000000..551fd5dc
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_project_limits_test.go
@@ -0,0 +1,29 @@
+package k6_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6ProjectLimits_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_project_limits/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					// project_id
+					resource.TestCheckResourceAttr("data.grafana_k6_project_limits.from_project_id", "vuh_max_per_month", "10000"),
+					resource.TestCheckResourceAttr("data.grafana_k6_project_limits.from_project_id", "vu_max_per_test", "10000"),
+					resource.TestCheckResourceAttr("data.grafana_k6_project_limits.from_project_id", "vu_browser_max_per_test", "1000"),
+					resource.TestCheckResourceAttr("data.grafana_k6_project_limits.from_project_id", "duration_max_per_test", "3600"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/data_source_k6_project_test.go b/internal/resources/k6/data_source_k6_project_test.go
new file mode 100644
index 00000000..fc85243f
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_project_test.go
@@ -0,0 +1,30 @@
+package k6_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6Project_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_project/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_project.from_id", "name", "Terraform Test Project"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "is_default"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "grafana_folder_uid"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "updated"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/data_source_k6_projects_test.go b/internal/resources/k6/data_source_k6_projects_test.go
new file mode 100644
index 00000000..0ab63d9f
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_projects_test.go
@@ -0,0 +1,47 @@
+package k6_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6Projects_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var (
+		project  k6.ProjectApiModel
+		project2 k6.ProjectApiModel
+	)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_projects/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.project", &project),
+					projectCheckExists.exists("grafana_k6_project.project_2", &project2),
+					// from_name.0
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_projects.from_name", "projects.0.name", "Terraform Test Project"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.is_default"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.grafana_folder_uid"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.updated"),
+					// from_name.1
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_projects.from_name", "projects.1.name", "Terraform Test Project"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.is_default"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.grafana_folder_uid"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.updated"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/resource_loadtest.go b/internal/resources/k6/resource_loadtest.go
new file mode 100644
index 00000000..dc78ebdc
--- /dev/null
+++ b/internal/resources/k6/resource_loadtest.go
@@ -0,0 +1,353 @@
+package k6
+
+import (
+	"context"
+	"io"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.ResourceWithConfigure   = (*loadTestResource)(nil)
+	_ resource.ResourceWithImportState = (*loadTestResource)(nil)
+)
+
+var (
+	resourceLoadTestName = "grafana_k6_load_test"
+	resourceLoadTestID   = common.NewResourceID(common.IntIDField("id"))
+)
+
+func resourceLoadTest() *common.Resource {
+	return common.NewResource(
+		common.CategoryK6,
+		resourceLoadTestName,
+		resourceLoadTestID,
+		&loadTestResource{},
+	).WithLister(k6ListerFunction(listLoadTests))
+}
+
+// loadTestResourceModel maps the resource schema data.
+type loadTestResourceModel struct {
+	ID                types.Int32  `tfsdk:"id"`
+	ProjectID         types.Int32  `tfsdk:"project_id"`
+	Name              types.String `tfsdk:"name"`
+	Script            types.String `tfsdk:"script"`
+	BaselineTestRunID types.Int32  `tfsdk:"baseline_test_run_id"`
+	Created           types.String `tfsdk:"created"`
+	Updated           types.String `tfsdk:"updated"`
+}
+
+// loadTestResource is the resource implementation.
+type loadTestResource struct {
+	basePluginFrameworkResource
+}
+
+// Metadata returns the resource type name.
+func (r *loadTestResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = resourceLoadTestName
+}
+
+// Schema defines the schema for the resource.
+func (r *loadTestResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Manages a k6 load test.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "Numeric identifier of the load test.",
+				Computed:    true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project this load test belongs to.",
+				Required:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the load test.",
+				Required:    true,
+			},
+			"script": schema.StringAttribute{
+				Description: "The k6 test script content. Can be provided inline or via the `file()` function.",
+				Required:    true,
+			},
+			"baseline_test_run_id": schema.Int32Attribute{
+				Description: "Identifier of a baseline test run used for results comparison.",
+				Optional:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "The date when the load test was created.",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "The date when the load test was last updated.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Create creates the resource and sets the Terraform state on success.
+func (r *loadTestResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	// Retrieve values from plan
+	var plan loadTestResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.LoadTestsAPI.ProjectsLoadTestsCreate(ctx, plan.ProjectID.ValueInt32()).
+		Name(plan.Name.ValueString()).
+		Script(io.NopCloser(strings.NewReader(plan.Script.ValueString()))).
+		XStackId(r.config.StackID)
+
+	// Create new load test
+	lt, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error creating load test",
+			"Could not create load test, unexpected error: "+err.Error(),
+		)
+		return
+	}
+
+	// Map response body to schema and populate Computed attribute values
+	plan.ID = types.Int32Value(lt.GetId())
+	plan.Name = types.StringValue(lt.GetName())
+	plan.ProjectID = types.Int32Value(lt.GetProjectId())
+	plan.BaselineTestRunID = handleBaselineTestRunID(lt.GetBaselineTestRunId())
+	plan.Created = types.StringValue(lt.GetCreated().Format(time.RFC3339Nano))
+	plan.Updated = types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano))
+
+	// Set state to fully populated data
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Read retrieves the resource information.
+func (r *loadTestResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	// Get current state
+	var state loadTestResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Retrieve the load test attributes
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.LoadTestsAPI.LoadTestsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	lt, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test",
+			"Could not read k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Retrieve the load test script content
+	scriptReq := r.client.LoadTestsAPI.LoadTestsScriptRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	script, _, err := scriptReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test script",
+			"Could not read k6 load test script with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	state.ID = types.Int32Value(lt.GetId())
+	state.Name = types.StringValue(lt.GetName())
+	state.ProjectID = types.Int32Value(lt.GetProjectId())
+	state.BaselineTestRunID = handleBaselineTestRunID(lt.GetBaselineTestRunId())
+	state.Script = types.StringValue(script)
+	state.Created = types.StringValue(lt.GetCreated().Format(time.RFC3339Nano))
+	state.Updated = types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano))
+
+	// Set refreshed state
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *loadTestResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	// Retrieve values from plan
+	var plan loadTestResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Get current state to retrieve the ID
+	var state loadTestResourceModel
+	diags = req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Generate API request body from plan
+	toUpdate := k6.NewPatchLoadTestApiModel()
+	toUpdate.SetName(plan.Name.ValueString())
+	if plan.BaselineTestRunID.IsNull() {
+		toUpdate.SetBaselineTestRunIdNil()
+	} else {
+		toUpdate.SetBaselineTestRunId(plan.BaselineTestRunID.ValueInt32())
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	updateReq := r.client.LoadTestsAPI.LoadTestsPartialUpdate(ctx, state.ID.ValueInt32()).
+		PatchLoadTestApiModel(toUpdate).
+		XStackId(r.config.StackID)
+
+	// Update the load test
+	_, err := updateReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error updating k6 load test",
+			"Could not update k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Update the script if it has changed
+	if !plan.Script.Equal(state.Script) {
+		scriptReq := r.client.LoadTestsAPI.LoadTestsScriptUpdate(ctx, state.ID.ValueInt32()).
+			Body(io.NopCloser(strings.NewReader(plan.Script.ValueString()))).
+			XStackId(r.config.StackID)
+
+		_, err = scriptReq.Execute()
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"Error updating k6 load test script",
+				"Could not update k6 load test script with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+			)
+			return
+		}
+	}
+
+	// Update resource state with updated items and timestamp
+	fetchReq := r.client.LoadTestsAPI.LoadTestsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	lt, _, err := fetchReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test",
+			"Could not read k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Retrieve the updated script content
+	scriptReq := r.client.LoadTestsAPI.LoadTestsScriptRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	script, _, err := scriptReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test script",
+			"Could not read k6 load test script with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	plan.ID = types.Int32Value(lt.GetId())
+	plan.Name = types.StringValue(lt.GetName())
+	plan.ProjectID = types.Int32Value(lt.GetProjectId())
+	plan.BaselineTestRunID = handleBaselineTestRunID(lt.GetBaselineTestRunId())
+	plan.Script = types.StringValue(script)
+	plan.Created = types.StringValue(lt.GetCreated().Format(time.RFC3339Nano))
+	plan.Updated = types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano))
+
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *loadTestResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	// Retrieve values from state
+	var state loadTestResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Delete existing load test
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	deleteReq := r.client.LoadTestsAPI.LoadTestsDestroy(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	_, err := deleteReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error deleting k6 load test",
+			"Could not delete k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+	}
+}
+
+func (r *loadTestResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	id, err := strconv.ParseInt(req.ID, 10, 32)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error importing k6 load test",
+			"Could not parse k6 load test id "+req.ID+": "+err.Error(),
+		)
+		return
+	}
+
+	resp.State.SetAttribute(ctx, path.Root("id"), types.Int32Value(int32(id)))
+
+	readReq := resource.ReadRequest{State: resp.State}
+	readResp := resource.ReadResponse{State: resp.State}
+
+	r.Read(ctx, readReq, &readResp)
+	resp.Diagnostics.Append(readResp.Diagnostics...)
+}
+
+// listLoadTests retrieves the list ids of all the existing load tests.
+func listLoadTests(ctx context.Context, client *k6.APIClient, config *k6providerapi.K6APIConfig) ([]string, error) {
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, config.Token)
+	resp, _, err := client.LoadTestsAPI.LoadTestsList(ctx).
+		XStackId(config.StackID).
+		Execute()
+	if err != nil {
+		return nil, err
+	}
+
+	var ids []string
+	for _, lt := range resp.Value {
+		ids = append(ids, strconv.Itoa(int(lt.GetId())))
+	}
+	return ids, nil
+}
diff --git a/internal/resources/k6/resource_loadtest_test.go b/internal/resources/k6/resource_loadtest_test.go
new file mode 100644
index 00000000..58e508cf
--- /dev/null
+++ b/internal/resources/k6/resource_loadtest_test.go
@@ -0,0 +1,111 @@
+package k6_test
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccLoadTest_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var (
+		project  k6.ProjectApiModel
+		loadTest k6.LoadTestApiModel
+	)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy: resource.ComposeTestCheckFunc(
+			loadTestCheckExists.destroyed(&loadTest),
+			projectCheckExists.destroyed(&project),
+		),
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "resources/grafana_k6_load_test/resource.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.load_test_project", &project),
+					loadTestCheckExists.exists("grafana_k6_load_test.test_load_test", &loadTest),
+					resource.TestMatchResourceAttr("grafana_k6_load_test.test_load_test", "id", defaultIDRegexp),
+					resource.TestCheckResourceAttr("grafana_k6_load_test.test_load_test", "name", "Terraform Test Load Test"),
+					resource.TestCheckResourceAttr("grafana_k6_load_test.test_load_test", "script", "export default function() {\n  console.log('Hello from k6!');\n}\n"),
+					testutils.CheckLister("grafana_k6_load_test.test_load_test"),
+				),
+			},
+			{
+				ResourceName:      "grafana_k6_load_test.test_load_test",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+			// Change the name and script of a load test. This shouldn't recreate the load test.
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_k6_load_test/resource.tf", map[string]string{
+					"Terraform Test Load Test":      "Terraform Test Load Test Updated",
+					"console.log('Hello from k6!')": "console.log('Hello from updated k6!')",
+				}),
+				Check: resource.ComposeTestCheckFunc(
+					testAccLoadTestWasntRecreated("grafana_k6_load_test.test_load_test", &loadTest),
+					testAccLoadTestUnchangedAttr("grafana_k6_load_test.test_load_test", "id", func() string { return strconv.Itoa(int(loadTest.GetId())) }),
+					resource.TestCheckResourceAttr("grafana_k6_load_test.test_load_test", "name", "Terraform Test Load Test Updated"),
+					resource.TestCheckResourceAttr("grafana_k6_load_test.test_load_test", "script", "export default function() {\n  console.log('Hello from updated k6!');\n}\n"),
+					testAccLoadTestUnchangedAttr("grafana_k6_load_test.test_load_test", "created", func() string { return loadTest.GetCreated().Truncate(time.Microsecond).Format(time.RFC3339Nano) }),
+				),
+			},
+		},
+	})
+}
+
+func testAccLoadTestUnchangedAttr(resName, attrName string, oldValueGetter func() string) resource.TestCheckFunc {
+	return resource.TestCheckResourceAttrWith(resName, attrName, func(newVal string) error {
+		if oldValue := oldValueGetter(); oldValue != newVal {
+			return fmt.Errorf("%s has changed: %s -> %s", attrName, oldValue, newVal)
+		}
+		return nil
+	})
+}
+
+func testAccLoadTestWasntRecreated(rn string, oldLoadTest *k6.LoadTestApiModel) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		newLoadTestResource, ok := s.RootModule().Resources[rn]
+		if !ok {
+			return fmt.Errorf("load test not found: %s", rn)
+		}
+		if newLoadTestResource.Primary.ID == "" {
+			return fmt.Errorf("load test id not set")
+		}
+		var newLoadTestID int32
+		if loadTestID, err := strconv.Atoi(newLoadTestResource.Primary.ID); err != nil {
+			return fmt.Errorf("could not convert load test id to integer: %s", err.Error())
+		} else if newLoadTestID, err = common.ToInt32(loadTestID); err != nil {
+			return fmt.Errorf("could not convert load test id to int32: %s", err.Error())
+		}
+
+		client := testutils.Provider.Meta().(*common.Client).K6APIClient
+		config := testutils.Provider.Meta().(*common.Client).K6APIConfig
+
+		ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+		newLoadTest, _, err := client.LoadTestsAPI.LoadTestsRetrieve(ctx, newLoadTestID).
+			XStackId(config.StackID).
+			Execute()
+		if err != nil {
+			return fmt.Errorf("error getting load test: %s", err)
+		}
+		if newLoadTest.Created != oldLoadTest.Created {
+			return fmt.Errorf("load test creation date has changed: %s -> %s", oldLoadTest.Created, newLoadTest.Created)
+		}
+		if !oldLoadTest.GetUpdated().Before(newLoadTest.GetUpdated()) {
+			return fmt.Errorf("load test update date hasn't changed: %s -> %s", oldLoadTest.Updated, newLoadTest.Updated)
+		}
+		return nil
+	}
+}
diff --git a/internal/resources/k6/resource_project_limits_test.go b/internal/resources/k6/resource_project_limits_test.go
new file mode 100644
index 00000000..df71b8ec
--- /dev/null
+++ b/internal/resources/k6/resource_project_limits_test.go
@@ -0,0 +1,62 @@
+package k6_test
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccProjectLimits_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var project k6.ProjectApiModel
+	var projectLimits k6.ProjectLimitsApiModel
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy: resource.ComposeTestCheckFunc(
+			projectCheckExists.destroyed(&project),
+			projectLimitsCheckExists.destroyed(&projectLimits),
+		),
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "resources/grafana_k6_project_limits/resource.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.test_project_limits", &project),
+					projectLimitsCheckExists.exists("grafana_k6_project_limits.test_limits", &projectLimits),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vuh_max_per_month", "1000"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vu_max_per_test", "100"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vu_browser_max_per_test", "10"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "duration_max_per_test", "3600"),
+				),
+			},
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_k6_project_limits/resource.tf", map[string]string{
+					"vuh_max_per_month       = 1000": "vuh_max_per_month       = 2000",
+				}),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.test_project_limits", &project),
+					projectLimitsCheckExists.exists("grafana_k6_project_limits.test_limits", &projectLimits),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vuh_max_per_month", "2000"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vu_max_per_test", "100"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vu_browser_max_per_test", "10"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "duration_max_per_test", "3600"),
+				),
+			},
+			{
+				ResourceName:      "grafana_k6_project_limits.test_limits",
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: func(s *terraform.State) (string, error) {
+					return strconv.Itoa(int(project.GetId())), nil
+				},
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/resource_project_test.go b/internal/resources/k6/resource_project_test.go
new file mode 100644
index 00000000..f6ad52ad
--- /dev/null
+++ b/internal/resources/k6/resource_project_test.go
@@ -0,0 +1,110 @@
+package k6_test
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+var defaultIDRegexp = regexp.MustCompile(`^\d{5,8}$`)
+
+func TestAccProject_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var project k6.ProjectApiModel
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy: resource.ComposeTestCheckFunc(
+			projectCheckExists.destroyed(&project),
+		),
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "resources/grafana_k6_project/resource.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.test_project", &project),
+					resource.TestMatchResourceAttr("grafana_k6_project.test_project", "id", defaultIDRegexp),
+					resource.TestCheckResourceAttr("grafana_k6_project.test_project", "name", "Terraform Test Project"),
+					resource.TestMatchResourceAttr("grafana_k6_project.test_project", "is_default", regexp.MustCompile(`^(true|false)$`)),
+					resource.TestCheckResourceAttrSet("grafana_k6_project.test_project", "grafana_folder_uid"),
+					testutils.CheckLister("grafana_k6_project.test_project"),
+				),
+			},
+			{
+				ResourceName:            "grafana_k6_project.test_project",
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"grafana_folder_uid"},
+			},
+			// Change the title of a project. This shouldn't recreate the project.
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_k6_project/resource.tf", map[string]string{
+					"Terraform Test Project": "Terraform Test Project Updated",
+				}),
+				Check: resource.ComposeTestCheckFunc(
+					testAccProjectWasntRecreated("grafana_k6_project.test_project", &project),
+					testAccProjectUnchangedAttr("grafana_k6_project.test_project", "id", func() string { return strconv.Itoa(int(project.GetId())) }),
+					resource.TestCheckResourceAttr("grafana_k6_project.test_project", "name", "Terraform Test Project Updated"),
+					testAccProjectUnchangedAttr("grafana_k6_project.test_project", "grafana_folder_uid", project.GetGrafanaFolderUid),
+					testAccProjectUnchangedAttr("grafana_k6_project.test_project", "created", func() string { return project.GetCreated().Truncate(time.Microsecond).Format(time.RFC3339Nano) }),
+				),
+			},
+		},
+	})
+}
+
+func testAccProjectUnchangedAttr(resName, attrName string, oldValueGetter func() string) resource.TestCheckFunc {
+	return resource.TestCheckResourceAttrWith(resName, attrName, func(newVal string) error {
+		if oldValue := oldValueGetter(); oldValue != newVal {
+			return fmt.Errorf("%s has changed: %s -> %s", attrName, oldValue, newVal)
+		}
+		return nil
+	})
+}
+
+func testAccProjectWasntRecreated(rn string, oldProject *k6.ProjectApiModel) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		newProjectResource, ok := s.RootModule().Resources[rn]
+		if !ok {
+			return fmt.Errorf("project not found: %s", rn)
+		}
+		if newProjectResource.Primary.ID == "" {
+			return fmt.Errorf("project id not set")
+		}
+		var newProjectID int32
+		if projectID, err := strconv.Atoi(newProjectResource.Primary.ID); err != nil {
+			return fmt.Errorf("could not convert project id to integer: %s", err.Error())
+		} else if newProjectID, err = common.ToInt32(projectID); err != nil {
+			return fmt.Errorf("could not convert project id to int32: %s", err.Error())
+		}
+
+		client := testutils.Provider.Meta().(*common.Client).K6APIClient
+		config := testutils.Provider.Meta().(*common.Client).K6APIConfig
+
+		ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+		newProject, _, err := client.ProjectsAPI.ProjectsRetrieve(ctx, newProjectID).
+			XStackId(config.StackID).
+			Execute()
+		if err != nil {
+			return fmt.Errorf("error getting project: %s", err)
+		}
+		if newProject.Created != oldProject.Created {
+			return fmt.Errorf("project creation date has changed: %s -> %s", oldProject.Created, newProject.Created)
+		}
+		if !oldProject.GetUpdated().Before(newProject.GetUpdated()) {
+			return fmt.Errorf("project update date hasn't changed: %s -> %s", oldProject.Updated, newProject.Updated)
+		}
+		return nil
+	}
+}
diff --git a/internal/testutils/provider.go b/internal/testutils/provider.go
index 4faeca65..1111241c 100644
--- a/internal/testutils/provider.go
+++ b/internal/testutils/provider.go
@@ -188,6 +188,7 @@ func CheckCloudInstanceTestsEnabled(t *testing.T) {
 	CheckEnvVarsSet(t,
 		"GRAFANA_URL",
 		"GRAFANA_AUTH",
+		"GRAFANA_K6_ACCESS_TOKEN",
 		"GRAFANA_SM_ACCESS_TOKEN",
 		"GRAFANA_ONCALL_ACCESS_TOKEN",
 		"GRAFANA_CLOUD_PROVIDER_URL",