Add generic Kube API based CRUD storage
Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
parent
3bc5952f7e
commit
b7d823a077
165
internal/crud/crud.go
Normal file
165
internal/crud/crud.go
Normal 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
635
internal/crud/crud_test.go
Normal 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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user