Files
grafana/pkg/storage/unified/apistore/secure_test.go
T

287 lines
11 KiB
Go

package apistore
import (
"context"
"encoding/json"
"fmt"
"slices"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/registry/apis/secret"
)
func TestSecureLifecycle(t *testing.T) {
resourceWithSecureValues := func(sv common.InlineSecureValues) utils.GrafanaMetaAccessor {
obj, err := utils.MetaAccessor(&unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "something.grafana.app/v1beta1",
"kind": "CustomKind",
"metadata": map[string]any{
"namespace": "default",
"name": "test",
},
"secure": sv,
},
})
require.NoError(t, err)
return obj
}
t.Run("create secure values", func(t *testing.T) {
secureStore := secret.NewMockInlineSecureValueSupport(t)
secureStore.On("CreateInline", mock.Anything, mock.Anything, common.RawSecureValue("SecretAAA")).
Return("NameForA", nil).Once()
secureStore.On("CreateInline", mock.Anything, mock.Anything, common.RawSecureValue("SecretBBB")).
Return("NameForB", nil).Once()
info := &objectForStorage{}
obj := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Create: "SecretAAA"},
"b": common.InlineSecureValue{Create: "SecretBBB"},
})
err := prepareSecureValues(context.Background(), secureStore, obj, nil, info)
require.NoError(t, err)
require.True(t, info.hasChanged)
slices.Sort(info.createdSecureValues) // keep a predictable order
require.Equal(t, []string{"NameForA", "NameForB"}, info.createdSecureValues)
secure, err := obj.GetSecureValues()
require.NoError(t, err)
require.JSONEq(t, `{
"a": {"name": "NameForA"},
"b": {"name": "NameForB"}
}`, asJSON(secure, true))
secureStore.AssertExpectations(t)
})
t.Run("create secure values with errors", func(t *testing.T) {
obj := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Create: "SecretAAA"},
"b": common.InlineSecureValue{Create: "SecretBBB"},
})
info := &objectForStorage{}
expectError := fmt.Errorf("expected error")
secureStore := secret.NewMockInlineSecureValueSupport(t)
secureStore.On("CreateInline", mock.Anything, mock.Anything, common.RawSecureValue("SecretAAA")).
Return("", expectError).Maybe()
secureStore.On("CreateInline", mock.Anything, mock.Anything, common.RawSecureValue("SecretBBB")).
Return("", expectError).Maybe()
err := prepareSecureValues(context.Background(), secureStore, obj, nil, info)
require.Error(t, err, "should error when secure value creation fails")
require.Equal(t, expectError, err, "error should be propagated")
secureStore.AssertExpectations(t)
})
t.Run("change name manually", func(t *testing.T) {
secureStore := secret.NewMockInlineSecureValueSupport(t)
info := &objectForStorage{}
previous := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Name: "111"},
"b": common.InlineSecureValue{Name: "222"},
"c": common.InlineSecureValue{Name: "333"},
})
obj := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Name: "222"},
"b": common.InlineSecureValue{Name: "333"}, // no change
// "c" will be loaded from the previous object without changes
})
err := prepareSecureValues(context.Background(), secureStore, obj, previous, info)
require.NoError(t, err)
require.True(t, info.hasChanged)
require.Empty(t, info.createdSecureValues)
require.Equal(t, info.deleteSecureValues, []string{"111"}) // will be removed if storage succeeds
secure, err := obj.GetSecureValues()
require.NoError(t, err)
require.JSONEq(t, `{
"a": {"name": "222"},
"b": {"name": "333"},
"c": {"name": "333"}
}`, asJSON(secure, true))
})
t.Run("update without secrets", func(t *testing.T) {
secureStore := secret.NewMockInlineSecureValueSupport(t)
info := &objectForStorage{}
previousObject := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Name: "NameForA"},
"b": common.InlineSecureValue{Name: "NameForB"},
})
objWithoutSecrets := resourceWithSecureValues(nil)
// Note that the secure values from the previous object are copied over
err := prepareSecureValues(context.Background(), secureStore, objWithoutSecrets, previousObject, info)
require.NoError(t, err)
require.False(t, info.hasChanged)
secure, err := objWithoutSecrets.GetSecureValues()
require.NoError(t, err)
require.JSONEq(t, `{
"a": {"name": "NameForA"},
"b": {"name": "NameForB"}
}`, asJSON(secure, true))
})
t.Run("remove secure values", func(t *testing.T) {
secureStore := secret.NewMockInlineSecureValueSupport(t)
previous := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Name: "NameForA"},
"b": common.InlineSecureValue{Name: "NameForB"},
"c": common.InlineSecureValue{Name: "NameForC"},
})
// Remove "b" with an explicit command
obj := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Name: "NameForA"}, // no change
"b": common.InlineSecureValue{Remove: true},
// "c" will be loaded from the previous object
})
// Prepare secure values does not change anything when removing
info := &objectForStorage{}
err := prepareSecureValues(context.Background(), secureStore, obj, previous, info)
require.NoError(t, err)
require.True(t, info.hasChanged) // value was removed
secureStore.AssertExpectations(t) // nothing called
secure, err := obj.GetSecureValues()
require.NoError(t, err)
require.JSONEq(t, `{
"a": {"name": "NameForA"},
"c": {"name": "NameForC"}
}`, asJSON(secure, true))
// When there is not an error, the finish command will do a real delete
owner := utils.ToObjectReference(obj)
secureStore.On("DeleteWhenOwnedByResource", mock.Anything, owner, "NameForB").
Return(nil).Once()
err = info.finish(context.Background(), nil, secureStore)
require.NoError(t, err)
require.True(t, info.hasChanged) // value was removed
secureStore.AssertExpectations(t) // nothing called
// When an error exists, no values will be deleted
err = fmt.Errorf("expected error")
outErr := info.finish(context.Background(), err, secureStore)
require.Equal(t, err, outErr, "error should be passed through")
})
t.Run("remove invalid secure values", func(t *testing.T) {
secureStore := secret.NewMockInlineSecureValueSupport(t)
obj := resourceWithSecureValues(common.InlineSecureValues{
"b": common.InlineSecureValue{Remove: true},
})
// Previous values must exist for remove to execute
info := &objectForStorage{}
err := prepareSecureValues(context.Background(), secureStore, obj, resourceWithSecureValues(nil), info)
require.Error(t, err, "should error when previous secure values does not exist")
require.Equal(t, "cannot remove secure value 'b', it did not exist in the previous value", err.Error())
secureStore.AssertExpectations(t)
})
t.Run("delete resource", func(t *testing.T) {
secureStore := secret.NewMockInlineSecureValueSupport(t)
obj := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Name: "NameForA"},
})
sv, err := obj.GetSecureValues()
require.NoError(t, err)
require.Len(t, sv, 1)
owner := utils.ToObjectReference(obj)
secureStore.On("DeleteWhenOwnedByResource", mock.Anything, owner, "NameForA").
Return(nil).Once()
err = handleSecureValuesDelete(context.Background(), secureStore, obj)
require.NoError(t, err)
secureStore.AssertExpectations(t)
sv, err = obj.GetSecureValues()
require.NoError(t, err)
require.Empty(t, sv, "secure values should be empty after delete")
// Delete should propagate deletion errors
obj = resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Name: "NameForA"},
})
expectError := fmt.Errorf("expected error")
secureStore = secret.NewMockInlineSecureValueSupport(t)
secureStore.On("DeleteWhenOwnedByResource", mock.Anything, owner, "NameForA").
Return(expectError).Once()
err = handleSecureValuesDelete(context.Background(), secureStore, obj)
require.Equal(t, expectError, err, "error should be passed through")
secureStore.AssertExpectations(t)
})
t.Run("invalid states", func(t *testing.T) {
secureStore := secret.NewMockInlineSecureValueSupport(t)
info := &objectForStorage{}
err := prepareSecureValues(context.Background(), secureStore, resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{}, // MUST have Create, Remove or Name
}), nil, info)
require.Error(t, err)
})
t.Run("setup errors", func(t *testing.T) {
objWithoutSecrets := resourceWithSecureValues(nil)
objWithCreateSecret := resourceWithSecureValues(common.InlineSecureValues{
"a": common.InlineSecureValue{Create: "SecretAAA"},
})
invalid, _ := utils.MetaAccessor(&unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "something.grafana.app/v1beta1",
"kind": "CustomKind",
"metadata": map[string]any{
"namespace": "default",
"name": "test",
},
"secure": t, // something NOT a secure value
},
})
info := &objectForStorage{}
err := prepareSecureValues(context.Background(), nil, invalid, nil, info)
require.Error(t, err, "should error when secure values are not a map")
err = prepareSecureValues(context.Background(), nil, objWithCreateSecret, invalid, info)
require.Error(t, err, "should error when previous secure values are not a map")
err = prepareSecureValues(context.Background(), nil, objWithCreateSecret, nil, info)
require.Error(t, err, "should error when secure value storage is not configured")
err = prepareSecureValues(context.Background(), nil, objWithCreateSecret, objWithoutSecrets, info)
require.Error(t, err, "should error when secure value storage is not configured")
err = prepareSecureValues(context.Background(), nil, objWithoutSecrets, objWithCreateSecret, info)
require.Error(t, err, "should error when previous value does not have a name")
// DELETE Setup errors
err = handleSecureValuesDelete(context.Background(), nil, invalid)
require.Error(t, err, "should error when secure values are not a map")
err = handleSecureValuesDelete(context.Background(), nil, objWithCreateSecret)
require.Error(t, err, "should error when secure value storage is not configured")
})
}
func asJSON(v any, pretty bool) string {
if v == nil {
return ""
}
if pretty {
bytes, _ := json.MarshalIndent(v, "", " ")
return string(bytes)
}
bytes, _ := json.Marshal(v)
return string(bytes)
}