Provisioning: Add inline secure values to repository schema (#109594)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 "" }}" }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user