Provisioning: Add Validation and Mutation for Connection resource (#115596)

* WIP: mutator added, start working on validator

* first validator iteration

* second validator iteration

* wip: working on integration tests

* re-working mutation and validation, using Connection interface

* fixing some rebase things

* fixing integration tests

* formatting

* fixing unit tests

* k8s codegen

* linting

* moving tests which are available only for enterprise

* addressing comments: using repo config for connections, updating tests

* addressing comments: adding some more info in the app and installation

* fixing app data

* addressing comments: updating connection implementation

* addressing comments

* formatting

* fixing tests
This commit is contained in:
Daniele Stefano Ferru
2026-01-12 17:52:00 +01:00
committed by GitHub
parent 0d1ec94548
commit e4b79e2fc8
40 changed files with 2798 additions and 533 deletions
+5
View File
@@ -14,6 +14,7 @@ import (
"testing"
"time"
githubConnection "github.com/grafana/grafana/apps/provisioning/pkg/connection/github"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
@@ -207,6 +208,10 @@ func (c *K8sTestHelper) GetEnv() server.TestEnv {
return c.env
}
func (c *K8sTestHelper) SetGithubConnectionFactory(f githubConnection.GithubFactory) {
c.env.GithubConnectionFactory = f
}
func (c *K8sTestHelper) GetListenerAddress() string {
return c.listenerAddress
}
@@ -4559,7 +4559,7 @@
}
]
},
"webhook": {
"token": {
"description": "Token is the reference of the token used to act as the Connection. This value is stored securely and cannot be read back",
"default": {},
"allOf": [
@@ -2,13 +2,13 @@ package provisioning
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
@@ -20,7 +20,7 @@ func TestIntegrationProvisioning_ConnectionRepositories(t *testing.T) {
helper := runGrafana(t)
ctx := context.Background()
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
// Create a connection for testing
connection := &unstructured.Unstructured{Object: map[string]any{
@@ -39,13 +39,12 @@ func TestIntegrationProvisioning_ConnectionRepositories(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.NoError(t, err, "failed to create connection")
_, err := helper.CreateGithubConnection(t, ctx, connection)
require.NoError(t, err)
t.Run("endpoint returns not implemented", func(t *testing.T) {
var statusCode int
@@ -129,14 +128,14 @@ func TestIntegrationProvisioning_ConnectionRepositoriesResponseType(t *testing.T
helper := runGrafana(t)
ctx := context.Background()
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
// Create a connection for testing
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "connection-repositories-type-test",
"name": "connection-repositories-test",
"namespace": "default",
},
"spec": map[string]any{
@@ -148,13 +147,12 @@ func TestIntegrationProvisioning_ConnectionRepositoriesResponseType(t *testing.T
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.NoError(t, err, "failed to create connection")
_, err := helper.CreateGithubConnection(t, ctx, connection)
require.NoError(t, err)
t.Run("verify ExternalRepositoryList type exists in API", func(t *testing.T) {
// Verify the type is registered and can be instantiated
@@ -2,12 +2,12 @@ package provisioning
import (
"context"
"encoding/base64"
"net/http"
"testing"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/grafana/grafana/pkg/util/testutil"
@@ -18,7 +18,7 @@ func TestIntegrationProvisioning_ConnectionStatusAuthorization(t *testing.T) {
helper := runGrafana(t)
ctx := context.Background()
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
// Create a connection for testing
connection := &unstructured.Unstructured{Object: map[string]any{
@@ -37,13 +37,12 @@ func TestIntegrationProvisioning_ConnectionStatusAuthorization(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.NoError(t, err, "failed to create connection")
_, err := helper.CreateGithubConnection(t, ctx, connection)
require.NoError(t, err)
t.Run("admin can GET connection status", func(t *testing.T) {
var statusCode int
+261 -26
View File
@@ -2,11 +2,20 @@ package provisioning
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/google/go-github/v70/github"
githubConnection "github.com/grafana/grafana/apps/provisioning/pkg/connection/github"
"github.com/grafana/grafana/pkg/extensions"
"github.com/grafana/grafana/pkg/util/testutil"
ghmock "github.com/migueleliasweb/go-github-mock/src/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
@@ -17,12 +26,55 @@ import (
clientset "github.com/grafana/grafana/apps/provisioning/pkg/generated/clientset/versioned"
)
//nolint:gosec // Test RSA private key (generated for testing purposes only)
const testPrivateKeyPEM = `-----BEGIN RSA PRIVATE KEY-----
MIIEoQIBAAKCAQBn1MuM5hIfH6d3TNStI1ofWv/gcjQ4joi9cFijEwVLuPYkF1nD
KkSbaMGFUWiOTaB/H9fxmd/V2u04NlBY3av6m5T/sHfVSiEWAEUblh3cA34HVCmD
cqyyVty5HLGJJlSs2C7W2x7yUc9ImzyDBsyjpKOXuojJ9wN9a17D2cYU5WkXjoDC
4BHid61jn9WBTtPZXSgOdirwahNzxZQSIP7DA9T8yiZwIWPp5YesgsAPyQLCFPgM
s77xz/CEUnEYQ35zI/k/mQrwKdQ/ZP8xLwQohUID0BIxE7G5quL069RuuCZWZkoF
oPiZbp7HSryz1+19jD3rFT7eHGUYvAyCnXmXAgMBAAECggEADSs4Bc7ITZo+Kytb
bfol3AQ2n8jcRrANN7mgBE7NRSVYUouDnvUlbnCC2t3QXPwLdxQa11GkygLSQ2bg
GeVDgq1o4GUJTcvxFlFCcpU/hEANI/DQsxNAQ/4wUGoLOlHaO3HPvwBblHA70gGe
Ux/xpG+lMAFAiB0EHEwZ4M0mClBEOQv3NzaFTWuBHtIMS8eid7M1q5qz9+rCgZSL
KBBHo0OvUbajG4CWl8SM6LUYapASGg+U17E+4xA3npwpIdsk+CbtX+vvX324n4kn
0EkrJqCjv8M1KiCKAP+UxwP00ywxOg4PN+x+dHI/I7xBvEKe/x6BltVSdGA+PlUK
02wagQKBgQDF7gdQLFIagPH7X7dBP6qEGxj/Ck9Qdz3S1gotPkVeq+1/UtQijYZ1
j44up/0yB2B9P4kW091n+iWcyfoU5UwBua9dHvCZP3QH05LR1ZscUHxLGjDPBASt
l2xSq0hqqNWBspb1M0eCY0Yxi65iDkj3xsI2iN35BEb1FlWdR5KGvwKBgQCGS0ce
wASWbZIPU2UoKGOQkIJU6QmLy0KZbfYkpyfE8IxGttYVEQ8puNvDDNZWHNf+LP85
c8iV6SfnWiLmu1XkG2YmJFBCCAWgJ8Mq2XQD8E+a/xcaW3NqlcC5+I2czX367j3r
69wZSxRbzR+DCfOiIkrekJImwN183ZYy2cBbKQKBgFj86IrSMmO6H5Ft+j06u5ZD
fJyF7Rz3T3NwSgkHWzbyQ4ggHEIgsRg/36P4YSzSBj6phyAdRwkNfUWdxXMJmH+a
FU7frzqnPaqbJAJ1cBRt10QI1XLtkpDdaJVObvONTtjOC3LYiEkGCzQRYeiyFXpZ
AU51gJ8JnkFotjtNR4KPAoGAehVREDlLcl0lnN0ZZspgyPk2Im6/iOA9KTH3xBZZ
ZwWu4FIyiHA7spgk4Ep5R0ttZ9oMI3SIcw/EgONGOy8uw/HMiPwWIhEc3B2JpRiO
CU6bb7JalFFyuQBudiHoyxVcY5PVovWF31CLr3DoJr4TR9+Y5H/U/XnzYCIo+w1N
exECgYBFAGKYTIeGAvhIvD5TphLpbCyeVLBIq5hRyrdRY+6Iwqdr5PGvLPKwin5+
+4CDhWPW4spq8MYPCRiMrvRSctKt/7FhVGL2vE/0VY3TcLk14qLC+2+0lnPVgnYn
u5/wOyuHp1cIBnjeN41/pluOWFBHI9xLW3ExLtmYMiecJ8VdRA==
-----END RSA PRIVATE KEY-----`
//nolint:gosec // Test RSA public key (generated for testing purposes only)
const testPublicKeyPem = `-----BEGIN PUBLIC KEY-----
MIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQBn1MuM5hIfH6d3TNStI1of
Wv/gcjQ4joi9cFijEwVLuPYkF1nDKkSbaMGFUWiOTaB/H9fxmd/V2u04NlBY3av6
m5T/sHfVSiEWAEUblh3cA34HVCmDcqyyVty5HLGJJlSs2C7W2x7yUc9ImzyDBsyj
pKOXuojJ9wN9a17D2cYU5WkXjoDC4BHid61jn9WBTtPZXSgOdirwahNzxZQSIP7D
A9T8yiZwIWPp5YesgsAPyQLCFPgMs77xz/CEUnEYQ35zI/k/mQrwKdQ/ZP8xLwQo
hUID0BIxE7G5quL069RuuCZWZkoFoPiZbp7HSryz1+19jD3rFT7eHGUYvAyCnXmX
AgMBAAE=
-----END PUBLIC KEY-----`
func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
ctx := context.Background()
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
decryptService := helper.GetEnv().DecryptService
require.NotNil(t, decryptService, "decrypt service not wired properly")
t.Run("should perform CRUDL requests on connection", func(t *testing.T) {
connection := &unstructured.Unstructured{Object: map[string]any{
@@ -41,12 +93,12 @@ func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
// CREATE
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
_, err := helper.CreateGithubConnection(t, ctx, connection)
require.NoError(t, err, "failed to create resource")
// READ
@@ -64,6 +116,22 @@ func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) {
require.Contains(t, output.Object, "secure", "object should contain secure")
assert.Contains(t, output.Object["secure"], "privateKey", "secure should contain PrivateKey")
// Verifying token
assert.Contains(t, output.Object["secure"], "token", "token should be created")
secretName, found, err := unstructured.NestedString(output.Object, "secure", "token", "name")
require.NoError(t, err, "error getting secret name")
require.True(t, found, "secret name should exist: %v", output.Object)
decrypted, err := decryptService.Decrypt(ctx, "provisioning.grafana.app", output.GetNamespace(), secretName)
require.NoError(t, err, "decryption error")
require.Len(t, decrypted, 1)
val := decrypted[secretName].Value()
require.NotNil(t, val)
k := val.DangerouslyExposeAndConsumeValue()
valid, err := verifyToken(t, "123456", testPublicKeyPem, k)
require.NoError(t, err, "error verifying token: %s", k)
require.True(t, valid, "token should be valid: %s", k)
// LIST
list, err := helper.Connections.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err, "failed to list resource")
@@ -81,22 +149,22 @@ func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) {
"spec": map[string]any{
"type": "github",
"github": map[string]any{
"appID": "456789",
"installationID": "454545",
"appID": "123456",
"installationID": "454546",
},
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
res, err := helper.Connections.Resource.Update(ctx, updatedConnection, metav1.UpdateOptions{})
res, err := helper.UpdateGithubConnection(t, ctx, updatedConnection)
require.NoError(t, err, "failed to update resource")
spec = res.Object["spec"].(map[string]any)
require.Contains(t, spec, "github")
githubInfo = spec["github"].(map[string]any)
assert.Equal(t, "456789", githubInfo["appID"], "appID should be updated")
assert.Equal(t, "454546", githubInfo["installationID"], "installationID should be updated")
// DELETE
require.NoError(t, helper.Connections.Resource.Delete(ctx, "connection", metav1.DeleteOptions{}), "failed to delete resource")
@@ -122,7 +190,7 @@ func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
@@ -155,9 +223,12 @@ func TestIntegrationProvisioning_ConnectionCRUDL(t *testing.T) {
}
func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := runGrafana(t)
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
ctx := context.Background()
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
t.Run("should fail when type is empty", func(t *testing.T) {
connection := &unstructured.Unstructured{Object: map[string]any{
@@ -172,13 +243,13 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "type must be specified")
assert.Contains(t, err.Error(), "connection type \"\" is not supported")
})
t.Run("should fail when type is invalid", func(t *testing.T) {
@@ -194,13 +265,57 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "spec.type: Unsupported value: \"some-invalid-type\"")
assert.Contains(t, err.Error(), "connection type \"some-invalid-type\" is not supported")
})
t.Run("should fail when type is 'git'", func(t *testing.T) {
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "connection",
"namespace": "default",
},
"spec": map[string]any{
"type": "git",
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "connection type \"git\" is not supported")
})
t.Run("should fail when type is 'local'", func(t *testing.T) {
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "connection",
"namespace": "default",
},
"spec": map[string]any{
"type": "local",
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "connection type \"local\" is not supported")
})
t.Run("should fail when type is github but 'github' field is not there", func(t *testing.T) {
@@ -216,13 +331,13 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "github info must be specified for GitHub connection")
assert.Contains(t, err.Error(), "invalid github connection")
})
t.Run("should fail when type is github but private key is not there", func(t *testing.T) {
@@ -246,7 +361,7 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
assert.Contains(t, err.Error(), "privateKey must be specified for GitHub connection")
})
t.Run("should fail when type is github but a client Secret is specified", func(t *testing.T) {
t.Run("should fail when type is github but a client Secret is also specified", func(t *testing.T) {
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
@@ -263,7 +378,7 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "someSecret",
"create": privateKeyBase64,
},
"clientSecret": map[string]any{
"create": "someSecret",
@@ -275,6 +390,100 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
assert.Contains(t, err.Error(), "clientSecret is forbidden in GitHub connection")
})
t.Run("should fail when type is github and github API is unavailable", func(t *testing.T) {
connectionFactory := helper.GetEnv().GithubConnectionFactory.(*githubConnection.Factory)
connectionFactory.Client = ghmock.NewMockedHTTPClient(
ghmock.WithRequestMatchHandler(
ghmock.GetApp,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
require.NoError(t, json.NewEncoder(w).Encode(github.ErrorResponse{
Response: &http.Response{
StatusCode: http.StatusServiceUnavailable,
},
Message: "Service unavailable",
}))
}),
),
)
helper.SetGithubConnectionFactory(connectionFactory)
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "connection",
"namespace": "default",
},
"spec": map[string]any{
"type": "github",
"github": map[string]any{
"appID": "123456",
"installationID": "454545",
},
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "spec.token: Internal error: github is unavailable")
})
t.Run("should fail when type is github and returned app ID doesn't match given one", func(t *testing.T) {
var appID int64 = 123455
appSlug := "appSlug"
connectionFactory := helper.GetEnv().GithubConnectionFactory.(*githubConnection.Factory)
connectionFactory.Client = ghmock.NewMockedHTTPClient(
ghmock.WithRequestMatch(
ghmock.GetApp, github.App{
ID: &appID,
Slug: &appSlug,
},
),
)
helper.SetGithubConnectionFactory(connectionFactory)
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Connection",
"metadata": map[string]any{
"name": "connection",
"namespace": "default",
},
"spec": map[string]any{
"type": "github",
"github": map[string]any{
"appID": "123456",
"installationID": "454545",
},
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "spec.appID: Invalid value: \"123456\": appID mismatch")
})
}
func TestIntegrationProvisioning_ConnectionEnterpriseValidation(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
if !extensions.IsEnterprise {
t.Skip("Skipping integration test when not enterprise")
}
helper := runGrafana(t)
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
ctx := context.Background()
t.Run("should fail when type is bitbucket but 'bitbucket' field is not there", func(t *testing.T) {
connection := &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "provisioning.grafana.app/v0alpha1",
@@ -294,7 +503,7 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "bitbucket info must be specified in Bitbucket connection")
assert.Contains(t, err.Error(), "invalid bitbucket connection")
})
t.Run("should fail when type is bitbucket but client secret is not there", func(t *testing.T) {
@@ -364,7 +573,7 @@ func TestIntegrationProvisioning_ConnectionValidation(t *testing.T) {
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
require.Error(t, err, "failed to create resource")
assert.Contains(t, err.Error(), "gitlab info must be specified in Gitlab connection")
assert.Contains(t, err.Error(), "invalid gitlab connection")
})
t.Run("should fail when type is gitlab but client secret is not there", func(t *testing.T) {
@@ -428,6 +637,7 @@ func TestIntegrationConnectionController_HealthCheckUpdates(t *testing.T) {
provisioningClient, err := clientset.NewForConfig(restConfig)
require.NoError(t, err)
connClient := provisioningClient.ProvisioningV0alpha1().Connections(namespace)
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
t.Run("health check gets updated after initial creation", func(t *testing.T) {
// Create a connection using unstructured (like other connection tests)
@@ -447,12 +657,12 @@ func TestIntegrationConnectionController_HealthCheckUpdates(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "test-private-key",
"create": privateKeyBase64,
},
},
}}
createdUnstructured, err := helper.Connections.Resource.Create(ctx, connUnstructured, metav1.CreateOptions{})
createdUnstructured, err := helper.CreateGithubConnection(t, ctx, connUnstructured)
require.NoError(t, err)
require.NotNil(t, createdUnstructured)
@@ -501,12 +711,12 @@ func TestIntegrationConnectionController_HealthCheckUpdates(t *testing.T) {
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "test-private-key-2",
"create": privateKeyBase64,
},
},
}}
createdUnstructured, err := helper.Connections.Resource.Create(ctx, connUnstructured, metav1.CreateOptions{})
createdUnstructured, err := helper.CreateGithubConnection(t, ctx, connUnstructured)
require.NoError(t, err)
require.NotNil(t, createdUnstructured)
@@ -538,7 +748,7 @@ func TestIntegrationConnectionController_HealthCheckUpdates(t *testing.T) {
updatedUnstructured := latestUnstructured.DeepCopy()
githubSpec := updatedUnstructured.Object["spec"].(map[string]any)["github"].(map[string]any)
githubSpec["appID"] = "99999"
_, err = helper.Connections.Resource.Update(ctx, updatedUnstructured, metav1.UpdateOptions{})
_, err = helper.UpdateGithubConnection(t, ctx, updatedUnstructured)
require.NoError(t, err)
// Wait for reconciliation after spec change
@@ -566,6 +776,7 @@ func TestIntegrationProvisioning_RepositoryFieldSelectorByConnection(t *testing.
helper := runGrafana(t)
ctx := context.Background()
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
privateKeyBase64 := base64.StdEncoding.EncodeToString([]byte(testPrivateKeyPEM))
// Create a connection first
connection := &unstructured.Unstructured{Object: map[string]any{
@@ -584,12 +795,12 @@ func TestIntegrationProvisioning_RepositoryFieldSelectorByConnection(t *testing.
},
"secure": map[string]any{
"privateKey": map[string]any{
"create": "test-private-key",
"create": privateKeyBase64,
},
},
}}
_, err := helper.Connections.Resource.Create(ctx, connection, createOptions)
_, err := helper.CreateGithubConnection(t, ctx, connection)
require.NoError(t, err, "failed to create connection")
t.Cleanup(func() {
@@ -731,3 +942,27 @@ func TestIntegrationProvisioning_RepositoryFieldSelectorByConnection(t *testing.
assert.Contains(t, names, "repo-with-different-connection")
})
}
func verifyToken(t *testing.T, appID, publicKey, token string) (bool, error) {
t.Helper()
// Parse the private key
key, err := jwt.ParseRSAPublicKeyFromPEM([]byte(publicKey))
if err != nil {
return false, err
}
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (any, error) {
return key, nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}))
if err != nil {
return false, err
}
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok || !parsedToken.Valid {
return false, fmt.Errorf("invalid token")
}
return claims.VerifyIssuer(appID, true), nil
}
+84 -2
View File
@@ -10,11 +10,14 @@ import (
"os"
"path"
"path/filepath"
"strconv"
"strings"
"testing"
"text/template"
"time"
"github.com/google/go-github/v70/github"
"github.com/grafana/grafana/pkg/extensions"
ghmock "github.com/migueleliasweb/go-github-mock/src/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -30,6 +33,7 @@ import (
dashboardsV2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
folder "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
provisioning "github.com/grafana/grafana/apps/provisioning/pkg/apis/provisioning/v0alpha1"
githubConnection "github.com/grafana/grafana/apps/provisioning/pkg/connection/github"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -699,13 +703,18 @@ func runGrafana(t *testing.T, options ...grafanaOption) *provisioningTestHelper
// (instance is needed for export jobs, folder for most operations)
ProvisioningAllowedTargets: []string{"folder", "instance"},
}
if extensions.IsEnterprise {
opts.ProvisioningRepositoryTypes = []string{"local", "github", "gitlab", "bitbucket"}
}
for _, o := range options {
o(&opts)
}
helper := apis.NewK8sTestHelper(t, opts)
// FIXME: keeping this line here to keep the dependency around until we have tests which use this again.
helper.GetEnv().GitHubFactory.Client = ghmock.NewMockedHTTPClient()
// FIXME: keeping these lines here to keep the dependency around until we have tests which use this again.
helper.GetEnv().GithubRepoFactory.Client = ghmock.NewMockedHTTPClient()
repositories := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
@@ -973,6 +982,79 @@ func (h *provisioningTestHelper) CleanupAllRepos(t *testing.T) {
}, waitTimeoutDefault, waitIntervalDefault, "repositories should be cleaned up between subtests")
}
func (h *provisioningTestHelper) CreateGithubConnection(
t *testing.T,
ctx context.Context,
connection *unstructured.Unstructured,
) (*unstructured.Unstructured, error) {
t.Helper()
err := h.setGithubClient(t, connection)
if err != nil {
return nil, err
}
return h.Connections.Resource.Create(ctx, connection, metav1.CreateOptions{FieldValidation: "Strict"})
}
func (h *provisioningTestHelper) UpdateGithubConnection(
t *testing.T,
ctx context.Context,
connection *unstructured.Unstructured,
) (*unstructured.Unstructured, error) {
t.Helper()
err := h.setGithubClient(t, connection)
if err != nil {
return nil, err
}
return h.Connections.Resource.Update(ctx, connection, metav1.UpdateOptions{FieldValidation: "Strict"})
}
func (h *provisioningTestHelper) setGithubClient(t *testing.T, connection *unstructured.Unstructured) error {
t.Helper()
objectSpec := connection.Object["spec"].(map[string]interface{})
githubObj := objectSpec["github"].(map[string]interface{})
appID := githubObj["appID"].(string)
id, err := strconv.ParseInt(appID, 10, 64)
if err != nil {
return err
}
appSlug := "someSlug"
connectionFactory := h.GetEnv().GithubConnectionFactory.(*githubConnection.Factory)
connectionFactory.Client = ghmock.NewMockedHTTPClient(
ghmock.WithRequestMatchHandler(
ghmock.GetApp,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
app := github.App{
ID: &id,
Slug: &appSlug,
}
_, _ = w.Write(ghmock.MustMarshal(app))
}),
),
ghmock.WithRequestMatchHandler(
ghmock.GetAppInstallationsByInstallationId,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("installation_id")
idInt, _ := strconv.ParseInt(id, 10, 64)
w.WriteHeader(http.StatusOK)
installation := github.Installation{
ID: &idInt,
}
_, _ = w.Write(ghmock.MustMarshal(installation))
}),
),
)
h.SetGithubConnectionFactory(connectionFactory)
return nil
}
func postHelper(t *testing.T, helper apis.K8sTestHelper, path string, body interface{}, user apis.User) (map[string]interface{}, int, error) {
return requestHelper(t, helper, http.MethodPost, path, body, user)
}
+14 -4
View File
@@ -10,6 +10,7 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/extensions"
provisioningAPIServer "github.com/grafana/grafana/pkg/registry/apis/provisioning"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -149,10 +150,19 @@ func TestIntegrationProvisioning_CreatingAndGetting(t *testing.T) {
}
}
assert.ElementsMatch(collect, []provisioning.RepositoryType{
provisioning.LocalRepositoryType,
provisioning.GitHubRepositoryType,
}, settings.AvailableRepositoryTypes)
if extensions.IsEnterprise {
assert.ElementsMatch(collect, []provisioning.RepositoryType{
provisioning.LocalRepositoryType,
provisioning.GitHubRepositoryType,
provisioning.BitbucketRepositoryType,
provisioning.GitLabRepositoryType,
}, settings.AvailableRepositoryTypes)
} else {
assert.ElementsMatch(collect, []provisioning.RepositoryType{
provisioning.LocalRepositoryType,
provisioning.GitHubRepositoryType,
}, settings.AvailableRepositoryTypes)
}
}, time.Second*10, time.Millisecond*100, "Expected settings to match")
})
+7
View File
@@ -622,6 +622,12 @@ func CreateGrafDir(t *testing.T, opts GrafanaOpts) (string, string) {
_, err = provisioningSect.NewKey("allowed_targets", strings.Join(opts.ProvisioningAllowedTargets, "|"))
require.NoError(t, err)
}
if len(opts.ProvisioningRepositoryTypes) > 0 {
provisioningSect, err := getOrCreateSection("provisioning")
require.NoError(t, err)
_, err = provisioningSect.NewKey("repository_types", strings.Join(opts.ProvisioningRepositoryTypes, "|"))
require.NoError(t, err)
}
if opts.EnableSCIM {
scimSection, err := getOrCreateSection("auth.scim")
require.NoError(t, err)
@@ -731,6 +737,7 @@ type GrafanaOpts struct {
UnifiedStorageMaxPageSizeBytes int
PermittedProvisioningPaths string
ProvisioningAllowedTargets []string
ProvisioningRepositoryTypes []string
GrafanaComSSOAPIToken string
LicensePath string
EnableRecordingRules bool