Provisioning: Add inline secure values to repository schema (#109594)

This commit is contained in:
Ryan McKinley
2025-08-20 09:05:41 +00:00
committed by GitHub
parent c37a03263f
commit fa81fae1e3
13 changed files with 333 additions and 28 deletions
@@ -2375,6 +2375,9 @@
"metadata": {
"default": {}
},
"secure": {
"default": {}
},
"spec": {
"default": {}
},
@@ -3464,6 +3467,14 @@
}
]
},
"secure": {
"default": {},
"allOf": [
{
"$ref": "#/components/schemas/com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.SecureValues"
}
]
},
"spec": {
"default": {},
"allOf": [
@@ -4193,6 +4204,30 @@
}
]
},
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.SecureValues": {
"description": "NOT YET USED FOR REAL -- testing secure value workflow",
"type": "object",
"properties": {
"token": {
"description": "Token used to connect the configured repository",
"default": {},
"allOf": [
{
"$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.InlineSecureValue"
}
]
},
"webhookSecret": {
"description": "Some webhooks (github) require a secret key value",
"default": {},
"allOf": [
{
"$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.InlineSecureValue"
}
]
}
}
},
"com.github.grafana.grafana.apps.provisioning.pkg.apis.provisioning.v0alpha1.SyncJobOptions": {
"type": "object",
"required": [
@@ -4406,6 +4441,45 @@
}
}
},
"com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.InlineSecureValue": {
"description": "Allow access to a secure value inside",
"oneOf": [
{
"required": [
"name"
]
},
{
"required": [
"create"
]
},
{
"required": [
"remove"
]
}
],
"properties": {
"create": {
"description": "Create a secure value -- this is only used for POST/PUT",
"type": "string",
"maxLength": 24576,
"minLength": 1
},
"name": {
"description": "Name in the secret service (reference)",
"type": "string",
"maxLength": 253,
"minLength": 1
},
"remove": {
"description": "Remove this value from the secure value map Values owned by this resource will be deleted if necessary",
"type": "boolean"
}
},
"additionalProperties": false
},
"com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": {
"type": "object",
"additionalProperties": true,
+12 -18
View File
@@ -1,15 +1,13 @@
package apis
import (
"context"
"encoding/json"
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/version"
apimachineryversion "k8s.io/apimachinery/pkg/version"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/testinfra"
@@ -38,18 +36,9 @@ func TestIntegrationOpenAPIs(t *testing.T) {
t.Run("check valid version response", func(t *testing.T) {
disco := h.NewDiscoveryClient()
req := disco.RESTClient().Get().
Prefix("version").
SetHeader("Accept", "application/json")
result := req.Do(context.Background())
require.NoError(t, result.Error())
raw, err := result.Raw()
require.NoError(t, err)
info := apimachineryversion.Info{}
err = json.Unmarshal(raw, &info)
info, err := disco.ServerVersion()
require.NoError(t, err)
require.Equal(t, runtime.Version(), info.GoVersion)
// Make sure the gitVersion is parsable
v, err := version.Parse(info.GitVersion)
@@ -57,10 +46,15 @@ func TestIntegrationOpenAPIs(t *testing.T) {
require.Equal(t, info.Major, fmt.Sprintf("%d", v.Major()))
require.Equal(t, info.Minor, fmt.Sprintf("%d", v.Minor()))
// Check that OpenAPI v2 (used by kubectl) returns properly
v2, err := disco.OpenAPISchema()
require.NoError(t, err, "requesting OpenAPI v2")
require.Equal(t, "Grafana API Server", v2.Info.Title)
// Check the v3 path resolves properly
// NOTE: fetching the v2 schema sometimes returns a 503 in our test infrastructure
// Removing the explicit `OneOf` properties from InlineSecureValue in:
// https://github.com/grafana/grafana/blob/main/pkg/apimachinery/apis/common/v0alpha1/secure_values.go#L78
// will consistently support V2, however kubectl and everything else continues to work
paths, err := disco.OpenAPIV3().Paths()
require.NoError(t, err, "requesting OpenAPI v3")
require.NotEmpty(t, paths, "has registered paths")
})
dir := "openapi_snapshots"
+94 -3
View File
@@ -9,14 +9,105 @@ import (
"testing"
"time"
"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"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestIntegrationProvisioning_InlineSecrets(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
helper := runGrafana(t, useAppPlatformSecrets)
createOptions := metav1.CreateOptions{FieldValidation: "Strict"}
ctx := context.Background()
decryptService := helper.GetEnv().DecryptService
require.NotNil(t, decryptService, "decrypt service wired properly")
type expectedField struct {
Path []string
DecryptedValue string // only try decrypting if not empty
}
tests := []struct {
name string
values map[string]any
inputFile string
expectedFields []expectedField
}{
{
name: "inline github token encrypted",
values: map[string]any{
"SecureTokenCreate": "some-token",
"SecureWebhookSecretCreate": "some-secret",
},
inputFile: "testdata/github-with-inline-secrets.json.tmpl",
expectedFields: []expectedField{
{
Path: []string{"secure", "token", "name"},
DecryptedValue: "some-token",
},
{
Path: []string{"secure", "webhookSecret", "name"},
DecryptedValue: "some-secret",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
input := helper.RenderObject(t, test.inputFile, test.values)
obj, err := helper.Repositories.Resource.Create(ctx, input, createOptions)
require.NoError(t, err, "failed to create resource")
require.True(t, strings.HasPrefix(obj.GetName(), "test-"), "created a unique name")
var created []string
// Move encrypted token mutation
for _, expectedField := range test.expectedFields {
name, found, err := unstructured.NestedString(obj.Object, expectedField.Path...)
require.NoError(t, err, "error getting expected path")
require.True(t, found, expectedField.Path)
require.NotEmpty(t, name, expectedField.Path)
created = append(created, name)
if expectedField.DecryptedValue != "" {
decrypted, err := decryptService.Decrypt(ctx, "provisioning.grafana.app", obj.GetNamespace(), name)
require.NoError(t, err, "decryption error")
require.Len(t, decrypted, 1)
val := decrypted[name].Value()
require.NotNil(t, val)
require.Equal(t, expectedField.DecryptedValue, val.DangerouslyExposeAndConsumeValue())
}
}
err = helper.Repositories.Resource.Delete(ctx, obj.GetName(), metav1.DeleteOptions{})
require.NoError(t, err, "failed to delete repository")
// Finalizers will be running async... so we need to wait until it is actually removed
require.Eventually(t, func() bool {
_, err := helper.Repositories.Resource.Get(ctx, obj.GetName(), metav1.GetOptions{})
return apierrors.IsNotFound(err)
}, time.Second*15, time.Millisecond*300, "should be removed")
// now check that we can no longer decrypt the requested values
results, err := decryptService.Decrypt(ctx, "provisioning.grafana.app", obj.GetNamespace(), created...)
require.NoError(t, err, "failed to execute decrypt with removed secrets")
for k, v := range results {
require.ErrorContains(t, v.Error(), "not found", "expecting not found error for all secrets: %s", k)
}
})
}
}
func TestIntegrationProvisioning_LegacySecrets(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@@ -0,0 +1,23 @@
{
"apiVersion": "provisioning.grafana.app/v0alpha1",
"kind": "Repository",
"metadata": {
"generateName": "test-"
},
"spec": {
"title": "title",
"description": "something",
"type": "github",
"github": {
"url": "{{ or .URL "https://github.com/grafana/grafana-git-sync-demo" }}",
"branch": "{{ or .Branch "integration-test" }}",
"generateDashboardPreviews": {{ if .GenerateDashboardPreviews }} true {{ else }} false {{ end }},
"token": "{{ or .Token "" }}",
"path": "{{ or .Path "grafana/" }}"
}
},
"secure": {
"token": { "create": "{{ or .SecureTokenCreate "" }}" },
"webhookSecret": { "create": "{{ or .SecureWebhookSecretCreate "" }}" }
}
}