Add generic Kube API based CRUD storage

Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
Monis Khan 2020-11-17 11:42:11 -05:00
parent 3bc5952f7e
commit b7d823a077
No known key found for this signature in database
GPG Key ID: 52C90ADA01B269B8
2 changed files with 800 additions and 0 deletions

165
internal/crud/crud.go Normal file
View File

@ -0,0 +1,165 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package crud
import (
"bytes"
"context"
"encoding/base32"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"go.pinniped.dev/internal/constable"
)
//nolint:gosec // ignore lint warnings that these are credentials
const (
secretNameFormat = "pinniped-storage-%s-%s"
secretLabelKey = "storage.pinniped.dev"
secretTypeFormat = "storage.pinniped.dev/%s"
secretVersion = "1"
secretDataKey = "pinniped-storage-data"
secretVersionKey = "pinniped-storage-version"
ErrSecretTypeMismatch = constable.Error("secret storage data has incorrect type")
ErrSecretLabelMismatch = constable.Error("secret storage data has incorrect label")
ErrSecretVersionMismatch = constable.Error("secret storage data has incorrect version") // TODO do we need this?
)
type Storage interface {
Create(ctx context.Context, signature string, data JSON) (resourceVersion string, err error)
Get(ctx context.Context, signature string, data JSON) (resourceVersion string, err error)
Update(ctx context.Context, signature, resourceVersion string, data JSON) (newResourceVersion string, err error)
Delete(ctx context.Context, signature string) error
}
type JSON interface{} // document that we need valid JSON types
func New(resource string, secrets corev1client.SecretInterface) Storage {
return &secretsStorage{
resource: resource,
secretType: corev1.SecretType(fmt.Sprintf(secretTypeFormat, resource)),
secretVersion: []byte(secretVersion),
secrets: secrets,
}
}
type secretsStorage struct {
resource string
secretType corev1.SecretType
secretVersion []byte
secrets corev1client.SecretInterface
}
func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON) (string, error) {
secret, err := s.toSecret(signature, "", data)
if err != nil {
return "", err
}
secret, err = s.secrets.Create(ctx, secret, metav1.CreateOptions{})
if err != nil {
return "", fmt.Errorf("failed to create %s for signature %s: %w", s.resource, signature, err)
}
return secret.ResourceVersion, nil
}
func (s *secretsStorage) Get(ctx context.Context, signature string, data JSON) (string, error) {
secret, err := s.secrets.Get(ctx, s.getName(signature), metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("failed to get %s for signature %s: %w", s.resource, signature, err)
}
if err := s.validateSecret(secret); err != nil {
return "", err
}
if err := json.Unmarshal(secret.Data[secretDataKey], data); err != nil {
return "", fmt.Errorf("failed to decode %s for signature %s: %w", s.resource, signature, err)
}
return secret.ResourceVersion, nil
}
func (s *secretsStorage) validateSecret(secret *corev1.Secret) error {
if secret.Type != s.secretType {
return fmt.Errorf("%w: %s must equal %s", ErrSecretTypeMismatch, secret.Type, s.secretType)
}
if labelResource := secret.Labels[secretLabelKey]; labelResource != s.resource {
return fmt.Errorf("%w: %s must equal %s", ErrSecretLabelMismatch, labelResource, s.resource)
}
if !bytes.Equal(secret.Data[secretVersionKey], s.secretVersion) {
return ErrSecretVersionMismatch // TODO should this be fatal or not?
}
return nil
}
func (s *secretsStorage) Update(ctx context.Context, signature, resourceVersion string, data JSON) (string, error) {
secret, err := s.toSecret(signature, resourceVersion, data)
if err != nil {
return "", err
}
secret, err = s.secrets.Update(ctx, secret, metav1.UpdateOptions{})
if err != nil {
return "", fmt.Errorf("failed to update %s for signature %s at resource version %s: %w", s.resource, signature, resourceVersion, err)
}
return secret.ResourceVersion, nil
}
func (s *secretsStorage) Delete(ctx context.Context, signature string) error {
if err := s.secrets.Delete(ctx, s.getName(signature), metav1.DeleteOptions{}); err != nil {
return fmt.Errorf("failed to delete %s for signature %s: %w", s.resource, signature, err)
}
return nil
}
//nolint: gochecknoglobals
var b32 = base32.StdEncoding.WithPadding(base32.NoPadding)
func (s *secretsStorage) getName(signature string) string {
// try to decode base64 signatures to prevent double encoding of binary data
signatureBytes := maybeBase64Decode(signature)
// lower case base32 encoding insures that our secret name is valid per ValidateSecretName in k/k
signatureAsValidName := strings.ToLower(b32.EncodeToString(signatureBytes))
return fmt.Sprintf(secretNameFormat, s.resource, signatureAsValidName)
}
func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON) (*corev1.Secret, error) {
buf, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to encode secret data for %s: %w", s.getName(signature), err)
}
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: s.getName(signature),
ResourceVersion: resourceVersion,
Labels: map[string]string{
secretLabelKey: s.resource, // make it easier to find this stuff via kubectl
},
OwnerReferences: nil, // TODO we should set this to make sure stuff gets clean up
},
Data: map[string][]byte{
secretDataKey: buf,
secretVersionKey: s.secretVersion,
},
Type: s.secretType,
}, nil
}
func maybeBase64Decode(signature string) []byte {
for _, encoding := range []*base64.Encoding{
// ordered in most likely used by HMAC, JWT, etc signatures
base64.RawURLEncoding,
base64.URLEncoding,
base64.RawStdEncoding,
base64.StdEncoding,
} {
if signatureBytes, err := encoding.DecodeString(signature); err == nil {
return signatureBytes
}
}
return []byte(signature)
}

635
internal/crud/crud_test.go Normal file
View File

@ -0,0 +1,635 @@
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package crud
import (
"context"
"errors"
"testing"
"github.com/ory/fosite/compose"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/fake"
coretesting "k8s.io/client-go/testing"
)
func TestStorage(t *testing.T) {
ctx := context.Background()
secretsGVR := schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "secrets",
}
type testJSON struct {
Data string
}
type mocker interface {
AddReactor(verb, resource string, reaction coretesting.ReactionFunc)
PrependReactor(verb, resource string, reaction coretesting.ReactionFunc)
Tracker() coretesting.ObjectTracker
}
hmac := compose.NewOAuth2HMACStrategy(&compose.Config{}, []byte("super-secret-32-byte-for-testing"), nil)
// test data generation via:
// code, signature, err := hmac.GenerateAuthorizeCode(ctx, nil)
validateSecretName := validation.NameIsDNSSubdomain // matches k/k
const (
namespace = "test-ns"
authorizationCode1 = "81qE408EKL-e99gcXo3UnXBz9W05yGm92_hBmvXeadM.R5h38Bmw7yOaWNy0ypB3feh9toM-3T2zlwMXQyeE9B0"
authorizationCode2 = "p7aIiOLy-btBBlCro5RWm1QABANKCiC0JmDPhUtfOY4.XXJsYsMWhnSMJi9TXJcPO6SDVO2R_QXImwroxxnQPA8"
authorizationCode3 = "skKp1RjGgIwZhT3vaB_k1F3cIj2yp7U8a7UD0xAaemU.5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I"
)
tests := []struct {
name string
resource string
mocks func(*testing.T, mocker)
run func(*testing.T, Storage) error
wantActions []coretesting.Action
wantSecrets []corev1.Secret
wantErr string
}{
{
name: "get non-existent",
resource: "authorization-codes",
mocks: nil,
run: func(t *testing.T, storage Storage) error {
_, err := storage.Get(ctx, "not-exists", nil)
return err
},
wantActions: []coretesting.Action{
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-authorization-codes-t2fx46yyvs3a"),
},
wantSecrets: nil,
wantErr: `failed to get authorization-codes for signature not-exists: secrets "pinniped-storage-authorization-codes-t2fx46yyvs3a" not found`,
},
{
name: "delete non-existent",
resource: "tokens",
mocks: nil,
run: func(t *testing.T, storage Storage) error {
return storage.Delete(ctx, "not-a-token")
},
wantActions: []coretesting.Action{
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-tokens-t2fx427lnci6s"),
},
wantSecrets: nil,
wantErr: `failed to delete tokens for signature not-a-token: secrets "pinniped-storage-tokens-t2fx427lnci6s" not found`,
},
{
name: "create and get",
resource: "access-tokens",
mocks: nil,
run: func(t *testing.T, storage Storage) error {
signature := hmac.AuthorizeCodeSignature(authorizationCode1)
require.NotEmpty(t, signature)
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
data := &testJSON{Data: "create-and-get"}
rv1, err := storage.Create(ctx, signature, data)
require.Empty(t, rv1) // fake client does not set this
require.NoError(t, err)
out := &testJSON{}
rv2, err := storage.Get(ctx, signature, out)
require.Empty(t, rv2) // fake client does not set this
require.NoError(t, err)
require.Equal(t, data, out)
return nil
},
wantActions: []coretesting.Action{
coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq",
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev": "access-tokens",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"create-and-get"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/access-tokens",
}),
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq"),
},
wantSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq",
Namespace: namespace,
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev": "access-tokens",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"create-and-get"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/access-tokens",
},
},
wantErr: "",
},
{
name: "get existing",
resource: "pandas-are-best",
mocks: func(t *testing.T, mock mocker) {
err := mock.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-pandas-are-best-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
Namespace: namespace,
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev": "pandas-are-best",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"snorlax"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/pandas-are-best",
})
require.NoError(t, err)
},
run: func(t *testing.T, storage Storage) error {
signature := hmac.AuthorizeCodeSignature(authorizationCode2)
require.NotEmpty(t, signature)
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
data := &testJSON{Data: "snorlax"}
out := &testJSON{}
rv1, err := storage.Get(ctx, signature, out)
require.Empty(t, rv1) // fake client does not set this
require.NoError(t, err)
require.Equal(t, data, out)
return nil
},
wantActions: []coretesting.Action{
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-pandas-are-best-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq"),
},
wantSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-pandas-are-best-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
Namespace: namespace,
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev": "pandas-are-best",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"snorlax"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/pandas-are-best",
},
},
wantErr: "",
},
{
name: "update existing",
resource: "stores",
mocks: func(t *testing.T, mock mocker) {
err := mock.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "35",
Labels: map[string]string{
"storage.pinniped.dev": "stores",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"pants"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/stores",
})
require.NoError(t, err)
mock.PrependReactor("update", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) {
secret := action.(coretesting.UpdateAction).GetObject().(*corev1.Secret)
secret.ResourceVersion = "45"
return false, nil, nil // we mutated the secret in place but we do not "handle" it
})
},
run: func(t *testing.T, storage Storage) error {
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
require.NotEmpty(t, signature)
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
data := &testJSON{Data: "pants"}
out := &testJSON{}
rv1, err := storage.Get(ctx, signature, out)
require.Equal(t, "35", rv1) // set in mock above
require.NoError(t, err)
require.Equal(t, data, out)
newData := &testJSON{Data: "shirts"}
rv2, err := storage.Update(ctx, signature, rv1, newData)
require.Equal(t, "45", rv2) // mock sets to a higher value on update
require.NoError(t, err)
newOut := &testJSON{}
rv3, err := storage.Get(ctx, signature, newOut)
require.Equal(t, "45", rv3) // we should see new rv now
require.NoError(t, err)
require.Equal(t, newData, newOut)
return nil
},
wantActions: []coretesting.Action{
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"),
coretesting.NewUpdateAction(secretsGVR, namespace, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
ResourceVersion: "35", // update at initial RV
Labels: map[string]string{
"storage.pinniped.dev": "stores",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"shirts"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/stores",
}),
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"),
},
wantSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "45", // final list at new RV
Labels: map[string]string{
"storage.pinniped.dev": "stores",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"shirts"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/stores",
},
},
wantErr: "",
},
{
name: "delete existing",
resource: "seals",
mocks: func(t *testing.T, mock mocker) {
err := mock.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq",
Namespace: namespace,
ResourceVersion: "",
Labels: map[string]string{
"storage.pinniped.dev": "seals",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"sad-seal"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/seals",
})
require.NoError(t, err)
},
run: func(t *testing.T, storage Storage) error {
signature := hmac.AuthorizeCodeSignature(authorizationCode2)
require.NotEmpty(t, signature)
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
return storage.Delete(ctx, signature)
},
wantActions: []coretesting.Action{
coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq"),
},
wantSecrets: nil,
wantErr: "",
},
{
name: "invalid exiting secret type",
resource: "candies",
mocks: func(t *testing.T, mock mocker) {
err := mock.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "55",
Labels: map[string]string{
"storage.pinniped.dev": "candies",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"twizzlers"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/candies-not",
})
require.NoError(t, err)
},
run: func(t *testing.T, storage Storage) error {
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
require.NotEmpty(t, signature)
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
out := &testJSON{}
rv1, err := storage.Get(ctx, signature, out)
require.Empty(t, rv1)
require.Empty(t, out.Data)
require.True(t, errors.Is(err, ErrSecretTypeMismatch))
require.EqualError(t, err, "secret storage data has incorrect type: storage.pinniped.dev/candies-not must equal storage.pinniped.dev/candies")
return nil
},
wantActions: []coretesting.Action{
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"),
},
wantSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "55",
Labels: map[string]string{
"storage.pinniped.dev": "candies",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"twizzlers"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/candies-not",
},
},
wantErr: "",
},
{
name: "invalid exiting secret wrong label",
resource: "candies",
mocks: func(t *testing.T, mock mocker) {
err := mock.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "55",
Labels: map[string]string{
"storage.pinniped.dev": "candies-are-bad",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"twizzlers"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/candies",
})
require.NoError(t, err)
},
run: func(t *testing.T, storage Storage) error {
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
require.NotEmpty(t, signature)
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
out := &testJSON{}
rv1, err := storage.Get(ctx, signature, out)
require.Empty(t, rv1)
require.Empty(t, out.Data)
require.True(t, errors.Is(err, ErrSecretLabelMismatch))
require.EqualError(t, err, "secret storage data has incorrect label: candies-are-bad must equal candies")
return nil
},
wantActions: []coretesting.Action{
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"),
},
wantSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "55",
Labels: map[string]string{
"storage.pinniped.dev": "candies-are-bad",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"twizzlers"}`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/candies",
},
},
wantErr: "",
},
{
name: "invalid exiting secret wrong version",
resource: "candies",
mocks: func(t *testing.T, mock mocker) {
err := mock.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "55",
Labels: map[string]string{
"storage.pinniped.dev": "candies",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"twizzlers"}`),
"pinniped-storage-version": []byte("77"),
},
Type: "storage.pinniped.dev/candies",
})
require.NoError(t, err)
},
run: func(t *testing.T, storage Storage) error {
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
require.NotEmpty(t, signature)
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
out := &testJSON{}
rv1, err := storage.Get(ctx, signature, out)
require.Empty(t, rv1)
require.Empty(t, out.Data)
require.True(t, errors.Is(err, ErrSecretVersionMismatch))
require.EqualError(t, err, "secret storage data has incorrect version")
return nil
},
wantActions: []coretesting.Action{
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"),
},
wantSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "55",
Labels: map[string]string{
"storage.pinniped.dev": "candies",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`{"Data":"twizzlers"}`),
"pinniped-storage-version": []byte("77"),
},
Type: "storage.pinniped.dev/candies",
},
},
wantErr: "",
},
{
name: "invalid exiting secret not json",
resource: "candies",
mocks: func(t *testing.T, mock mocker) {
err := mock.Tracker().Add(&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "55",
Labels: map[string]string{
"storage.pinniped.dev": "candies",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`}}bad data{{`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/candies",
})
require.NoError(t, err)
},
run: func(t *testing.T, storage Storage) error {
signature := hmac.AuthorizeCodeSignature(authorizationCode3)
require.NotEmpty(t, signature)
require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is
out := &testJSON{}
rv1, err := storage.Get(ctx, signature, out)
require.Empty(t, rv1)
require.Empty(t, out.Data)
require.EqualError(t, err, "failed to decode candies for signature 5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I: invalid character '}' looking for beginning of value")
return nil
},
wantActions: []coretesting.Action{
coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"),
},
wantSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba",
Namespace: namespace,
ResourceVersion: "55",
Labels: map[string]string{
"storage.pinniped.dev": "candies",
},
},
Data: map[string][]byte{
"pinniped-storage-data": []byte(`}}bad data{{`),
"pinniped-storage-version": []byte("1"),
},
Type: "storage.pinniped.dev/candies",
},
},
wantErr: "",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
client := fake.NewSimpleClientset()
if tt.mocks != nil {
tt.mocks(t, client)
}
secrets := client.CoreV1().Secrets(namespace)
storage := New(tt.resource, secrets)
err := tt.run(t, storage)
require.Equal(t, tt.wantErr, errString(err))
require.Equal(t, tt.wantActions, client.Actions())
checkSecretActionNames(t, client.Actions())
actualSecrets, err := secrets.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
require.Equal(t, tt.wantSecrets, actualSecrets.Items)
checkSecretListNames(t, actualSecrets.Items)
})
}
}
func checkSecretActionNames(t *testing.T, actions []coretesting.Action) {
t.Helper()
for _, action := range actions {
name := getName(t, action)
assertValidName(t, name)
}
}
func checkSecretListNames(t *testing.T, secrets []corev1.Secret) {
t.Helper()
for _, secret := range secrets {
assertValidName(t, secret.Name)
}
}
func assertValidName(t *testing.T, name string) {
t.Helper()
validateSecretName := validation.NameIsDNSSubdomain // matches k/k
require.NotEmpty(t, name)
require.Empty(t, validateSecretName(name, false))
require.Empty(t, validateSecretName(name, true)) // I do not think we actually care about this case
}
func getName(t *testing.T, action coretesting.Action) string {
t.Helper()
if getter, ok := action.(interface {
GetName() string
}); ok {
return getter.GetName()
}
if getter, ok := action.(interface {
GetObject() runtime.Object
}); ok {
accessor, err := meta.Accessor(getter.GetObject())
require.NoError(t, err)
return accessor.GetName()
}
t.Fatalf("failed to get name for action: %#v", action)
panic("unreachable")
}
func errString(err error) string {
if err == nil {
return ""
}
return err.Error()
}