2020-10-14 13:47:34 +00:00
|
|
|
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
package supervisorconfig
|
|
|
|
|
2020-10-14 20:41:16 +00:00
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/pem"
|
|
|
|
"errors"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
kubeinformers "k8s.io/client-go/informers"
|
|
|
|
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
|
|
|
kubetesting "k8s.io/client-go/testing"
|
|
|
|
|
2020-10-30 20:09:14 +00:00
|
|
|
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/config/v1alpha1"
|
|
|
|
pinnipedfake "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned/fake"
|
|
|
|
pinnipedinformers "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions"
|
2020-10-14 20:41:16 +00:00
|
|
|
"go.pinniped.dev/internal/controllerlib"
|
|
|
|
"go.pinniped.dev/internal/testutil"
|
|
|
|
)
|
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
func TestJWKSWriterControllerFilterSecret(t *testing.T) {
|
2020-10-14 20:41:16 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
2020-12-18 22:55:05 +00:00
|
|
|
secret metav1.Object
|
2020-10-14 20:41:16 +00:00
|
|
|
wantAdd bool
|
|
|
|
wantUpdate bool
|
|
|
|
wantDelete bool
|
|
|
|
wantParent controllerlib.Key
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "no owner reference",
|
2020-12-18 22:55:05 +00:00
|
|
|
secret: &corev1.Secret{
|
|
|
|
Type: "secrets.pinniped.dev/federation-domain-jwks",
|
2020-10-14 20:41:16 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "owner reference without correct APIVersion",
|
2020-12-18 22:55:05 +00:00
|
|
|
secret: &corev1.Secret{
|
|
|
|
Type: "secrets.pinniped.dev/federation-domain-jwks",
|
2020-10-14 20:41:16 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Namespace: "some-namespace",
|
|
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
|
|
{
|
2020-12-16 22:27:09 +00:00
|
|
|
Kind: "FederationDomain",
|
2020-10-14 20:41:16 +00:00
|
|
|
Name: "some-name",
|
|
|
|
Controller: boolPtr(true),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "owner reference without correct Kind",
|
2020-12-18 22:55:05 +00:00
|
|
|
secret: &corev1.Secret{
|
|
|
|
Type: "secrets.pinniped.dev/federation-domain-jwks",
|
2020-10-14 20:41:16 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Namespace: "some-namespace",
|
|
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
|
|
{
|
|
|
|
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
|
|
|
Name: "some-name",
|
|
|
|
Controller: boolPtr(true),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "owner reference without controller set to true",
|
2020-12-18 22:55:05 +00:00
|
|
|
secret: &corev1.Secret{
|
|
|
|
Type: "secrets.pinniped.dev/federation-domain-jwks",
|
2020-10-14 20:41:16 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Namespace: "some-namespace",
|
|
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
|
|
{
|
|
|
|
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
2020-12-16 22:27:09 +00:00
|
|
|
Kind: "FederationDomain",
|
2020-10-14 20:41:16 +00:00
|
|
|
Name: "some-name",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "correct owner reference",
|
2020-12-18 22:55:05 +00:00
|
|
|
secret: &corev1.Secret{
|
|
|
|
Type: "secrets.pinniped.dev/federation-domain-jwks",
|
2020-10-14 20:41:16 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Namespace: "some-namespace",
|
|
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
|
|
{
|
|
|
|
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
2020-12-16 22:27:09 +00:00
|
|
|
Kind: "FederationDomain",
|
2020-10-14 20:41:16 +00:00
|
|
|
Name: "some-name",
|
|
|
|
Controller: boolPtr(true),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
wantAdd: true,
|
|
|
|
wantUpdate: true,
|
|
|
|
wantDelete: true,
|
|
|
|
wantParent: controllerlib.Key{Namespace: "some-namespace", Name: "some-name"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "multiple owner references",
|
2020-12-18 22:55:05 +00:00
|
|
|
secret: &corev1.Secret{
|
|
|
|
Type: "secrets.pinniped.dev/federation-domain-jwks",
|
2020-10-14 20:41:16 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Namespace: "some-namespace",
|
|
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
|
|
{
|
|
|
|
Kind: "UnrelatedKind",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
2020-12-16 22:27:09 +00:00
|
|
|
Kind: "FederationDomain",
|
2020-10-14 20:41:16 +00:00
|
|
|
Name: "some-name",
|
|
|
|
Controller: boolPtr(true),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
wantAdd: true,
|
|
|
|
wantUpdate: true,
|
|
|
|
wantDelete: true,
|
|
|
|
wantParent: controllerlib.Key{Namespace: "some-namespace", Name: "some-name"},
|
|
|
|
},
|
2020-12-18 22:55:05 +00:00
|
|
|
{
|
|
|
|
name: "correct owner reference but wrong type",
|
|
|
|
secret: &corev1.Secret{
|
|
|
|
Type: "secrets.pinniped.dev/some-other-type",
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Namespace: "some-namespace",
|
|
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
|
|
{
|
|
|
|
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
|
|
|
Kind: "FederationDomain",
|
|
|
|
Name: "some-name",
|
|
|
|
Controller: boolPtr(true),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "resource of wrong data type",
|
|
|
|
secret: &corev1.Namespace{
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
Name: "some-namespace",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2020-10-14 20:41:16 +00:00
|
|
|
}
|
|
|
|
for _, test := range tests {
|
|
|
|
test := test
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
secretInformer := kubeinformers.NewSharedInformerFactory(
|
|
|
|
kubernetesfake.NewSimpleClientset(),
|
|
|
|
0,
|
|
|
|
).Core().V1().Secrets()
|
2020-12-17 19:34:49 +00:00
|
|
|
federationDomainInformer := pinnipedinformers.NewSharedInformerFactory(
|
2020-10-14 20:41:16 +00:00
|
|
|
pinnipedfake.NewSimpleClientset(),
|
|
|
|
0,
|
2020-12-16 22:27:09 +00:00
|
|
|
).Config().V1alpha1().FederationDomains()
|
2020-10-14 20:41:16 +00:00
|
|
|
withInformer := testutil.NewObservableWithInformerOption()
|
2020-10-17 00:51:40 +00:00
|
|
|
_ = NewJWKSWriterController(
|
2020-10-15 19:40:56 +00:00
|
|
|
nil, // labels, not needed
|
2020-10-14 20:41:16 +00:00
|
|
|
nil, // kubeClient, not needed
|
|
|
|
nil, // pinnipedClient, not needed
|
|
|
|
secretInformer,
|
2020-12-17 19:34:49 +00:00
|
|
|
federationDomainInformer,
|
2020-10-14 20:41:16 +00:00
|
|
|
withInformer.WithInformer,
|
|
|
|
)
|
|
|
|
|
|
|
|
unrelated := corev1.Secret{}
|
|
|
|
filter := withInformer.GetFilterForInformer(secretInformer)
|
2020-12-18 22:55:05 +00:00
|
|
|
require.Equal(t, test.wantAdd, filter.Add(test.secret))
|
|
|
|
require.Equal(t, test.wantUpdate, filter.Update(&unrelated, test.secret))
|
|
|
|
require.Equal(t, test.wantUpdate, filter.Update(test.secret, &unrelated))
|
|
|
|
require.Equal(t, test.wantDelete, filter.Delete(test.secret))
|
|
|
|
require.Equal(t, test.wantParent, filter.Parent(test.secret))
|
2020-10-14 20:41:16 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-17 19:34:49 +00:00
|
|
|
func TestJWKSWriterControllerFilterFederationDomain(t *testing.T) {
|
2020-10-14 20:41:16 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
tests := []struct {
|
2020-12-17 19:34:49 +00:00
|
|
|
name string
|
|
|
|
federationDomain configv1alpha1.FederationDomain
|
|
|
|
wantAdd bool
|
|
|
|
wantUpdate bool
|
|
|
|
wantDelete bool
|
|
|
|
wantParent controllerlib.Key
|
2020-10-14 20:41:16 +00:00
|
|
|
}{
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
name: "anything goes",
|
|
|
|
federationDomain: configv1alpha1.FederationDomain{},
|
|
|
|
wantAdd: true,
|
|
|
|
wantUpdate: true,
|
|
|
|
wantDelete: true,
|
|
|
|
wantParent: controllerlib.Key{},
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, test := range tests {
|
|
|
|
test := test
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
secretInformer := kubeinformers.NewSharedInformerFactory(
|
|
|
|
kubernetesfake.NewSimpleClientset(),
|
|
|
|
0,
|
|
|
|
).Core().V1().Secrets()
|
2020-12-17 19:34:49 +00:00
|
|
|
federationDomainInformer := pinnipedinformers.NewSharedInformerFactory(
|
2020-10-14 20:41:16 +00:00
|
|
|
pinnipedfake.NewSimpleClientset(),
|
|
|
|
0,
|
2020-12-16 22:27:09 +00:00
|
|
|
).Config().V1alpha1().FederationDomains()
|
2020-10-14 20:41:16 +00:00
|
|
|
withInformer := testutil.NewObservableWithInformerOption()
|
2020-10-17 00:51:40 +00:00
|
|
|
_ = NewJWKSWriterController(
|
2020-10-15 19:40:56 +00:00
|
|
|
nil, // labels, not needed
|
2020-10-14 20:41:16 +00:00
|
|
|
nil, // kubeClient, not needed
|
|
|
|
nil, // pinnipedClient, not needed
|
|
|
|
secretInformer,
|
2020-12-17 19:34:49 +00:00
|
|
|
federationDomainInformer,
|
2020-10-14 20:41:16 +00:00
|
|
|
withInformer.WithInformer,
|
|
|
|
)
|
|
|
|
|
2020-12-16 22:27:09 +00:00
|
|
|
unrelated := configv1alpha1.FederationDomain{}
|
2020-12-17 19:34:49 +00:00
|
|
|
filter := withInformer.GetFilterForInformer(federationDomainInformer)
|
|
|
|
require.Equal(t, test.wantAdd, filter.Add(&test.federationDomain))
|
|
|
|
require.Equal(t, test.wantUpdate, filter.Update(&unrelated, &test.federationDomain))
|
|
|
|
require.Equal(t, test.wantUpdate, filter.Update(&test.federationDomain, &unrelated))
|
|
|
|
require.Equal(t, test.wantDelete, filter.Delete(&test.federationDomain))
|
|
|
|
require.Equal(t, test.wantParent, filter.Parent(&test.federationDomain))
|
2020-10-14 20:41:16 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2020-10-14 13:47:34 +00:00
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
func TestJWKSWriterControllerSync(t *testing.T) {
|
2020-10-14 20:41:16 +00:00
|
|
|
// We shouldn't run this test in parallel since it messes with a global function (generateKey).
|
|
|
|
|
|
|
|
const namespace = "tuna-namespace"
|
|
|
|
|
2020-10-15 15:33:08 +00:00
|
|
|
goodKeyPEM, err := ioutil.ReadFile("testdata/good-ec-key.pem")
|
2020-10-14 20:41:16 +00:00
|
|
|
require.NoError(t, err)
|
2020-10-15 15:33:08 +00:00
|
|
|
block, _ := pem.Decode(goodKeyPEM)
|
|
|
|
require.NotNil(t, block, "expected block to be non-nil...is goodKeyPEM a valid PEM?")
|
|
|
|
goodKey, err := x509.ParseECPrivateKey(block.Bytes)
|
2020-10-14 20:41:16 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-12-17 19:34:49 +00:00
|
|
|
federationDomainGVR := schema.GroupVersionResource{
|
2020-10-14 20:41:16 +00:00
|
|
|
Group: configv1alpha1.SchemeGroupVersion.Group,
|
|
|
|
Version: configv1alpha1.SchemeGroupVersion.Version,
|
2020-12-16 22:27:09 +00:00
|
|
|
Resource: "federationdomains",
|
2020-10-14 20:41:16 +00:00
|
|
|
}
|
|
|
|
|
2020-12-17 19:34:49 +00:00
|
|
|
goodFederationDomain := &configv1alpha1.FederationDomain{
|
2020-10-14 20:41:16 +00:00
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
2020-12-17 19:34:49 +00:00
|
|
|
Name: "good-federationDomain",
|
2020-10-14 20:41:16 +00:00
|
|
|
Namespace: namespace,
|
2020-12-17 19:34:49 +00:00
|
|
|
UID: "good-federationDomain-uid",
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
2020-12-16 22:27:09 +00:00
|
|
|
Spec: configv1alpha1.FederationDomainSpec{
|
2020-10-14 20:41:16 +00:00
|
|
|
Issuer: "https://some-issuer.com",
|
|
|
|
},
|
|
|
|
}
|
2020-12-17 19:34:49 +00:00
|
|
|
goodFederationDomainWithStatus := goodFederationDomain.DeepCopy()
|
|
|
|
goodFederationDomainWithStatus.Status.Secrets.JWKS.Name = goodFederationDomainWithStatus.Name + "-jwks"
|
2020-10-14 20:41:16 +00:00
|
|
|
|
|
|
|
secretGVR := schema.GroupVersionResource{
|
|
|
|
Group: corev1.SchemeGroupVersion.Group,
|
|
|
|
Version: corev1.SchemeGroupVersion.Version,
|
|
|
|
Resource: "secrets",
|
|
|
|
}
|
|
|
|
|
|
|
|
newSecret := func(activeJWKPath, jwksPath string) *corev1.Secret {
|
|
|
|
s := corev1.Secret{
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
2020-12-17 19:34:49 +00:00
|
|
|
Name: goodFederationDomainWithStatus.Status.Secrets.JWKS.Name,
|
2020-10-14 20:41:16 +00:00
|
|
|
Namespace: namespace,
|
2020-10-15 19:40:56 +00:00
|
|
|
Labels: map[string]string{
|
|
|
|
"myLabelKey1": "myLabelValue1",
|
|
|
|
"myLabelKey2": "myLabelValue2",
|
|
|
|
},
|
2020-10-14 20:41:16 +00:00
|
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
APIVersion: federationDomainGVR.GroupVersion().String(),
|
2020-12-16 22:27:09 +00:00
|
|
|
Kind: "FederationDomain",
|
2020-12-17 19:34:49 +00:00
|
|
|
Name: goodFederationDomain.Name,
|
|
|
|
UID: goodFederationDomain.UID,
|
2020-10-14 20:41:16 +00:00
|
|
|
BlockOwnerDeletion: boolPtr(true),
|
|
|
|
Controller: boolPtr(true),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2020-12-17 22:48:49 +00:00
|
|
|
Type: "secrets.pinniped.dev/federation-domain-jwks",
|
2020-10-14 20:41:16 +00:00
|
|
|
}
|
|
|
|
s.Data = make(map[string][]byte)
|
|
|
|
if activeJWKPath != "" {
|
2020-10-17 00:51:40 +00:00
|
|
|
s.Data["activeJWK"] = readJWKJSON(t, activeJWKPath)
|
2020-10-14 20:41:16 +00:00
|
|
|
}
|
|
|
|
if jwksPath != "" {
|
2020-10-17 00:51:40 +00:00
|
|
|
s.Data["jwks"] = readJWKJSON(t, jwksPath)
|
2020-10-14 20:41:16 +00:00
|
|
|
}
|
|
|
|
return &s
|
|
|
|
}
|
|
|
|
|
|
|
|
goodSecret := newSecret("testdata/good-jwk.json", "testdata/good-jwks.json")
|
|
|
|
|
2020-12-17 22:48:49 +00:00
|
|
|
secretWithWrongType := newSecret("testdata/good-jwk.json", "testdata/good-jwks.json")
|
|
|
|
secretWithWrongType.Type = "not-the-right-type"
|
|
|
|
|
2020-10-14 20:41:16 +00:00
|
|
|
tests := []struct {
|
2020-12-17 19:34:49 +00:00
|
|
|
name string
|
|
|
|
key controllerlib.Key
|
|
|
|
secrets []*corev1.Secret
|
|
|
|
configKubeClient func(*kubernetesfake.Clientset)
|
|
|
|
configPinnipedClient func(*pinnipedfake.Clientset)
|
|
|
|
federationDomains []*configv1alpha1.FederationDomain
|
|
|
|
generateKeyErr error
|
|
|
|
wantGenerateKeyCount int
|
|
|
|
wantSecretActions []kubetesting.Action
|
|
|
|
wantFederationDomainActions []kubetesting.Action
|
|
|
|
wantError string
|
2020-10-14 20:41:16 +00:00
|
|
|
}{
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
name: "new federationDomain with no secret",
|
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomain,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewCreateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
|
|
|
kubetesting.NewUpdateAction(federationDomainGVR, namespace, goodFederationDomainWithStatus),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
name: "federationDomain without status with existing secret",
|
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomain,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
goodSecret,
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
|
|
|
kubetesting.NewUpdateAction(federationDomainGVR, namespace, goodFederationDomainWithStatus),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
name: "existing federationDomain with no secret",
|
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewCreateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
name: "existing federationDomain with existing secret",
|
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
goodSecret,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
name: "deleted federationDomain",
|
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
2020-10-14 20:41:16 +00:00
|
|
|
// Nothing to do here since Kube will garbage collect our child secret via its OwnerReference.
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "missing jwk in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("", "testdata/good-jwks.json"),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "missing jwks in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("testdata/good-jwk.json", ""),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-12-17 22:48:49 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "wrong type in secret",
|
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
secretWithWrongType,
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "invalid jwk JSON in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("testdata/not-json.txt", "testdata/good-jwks.json"),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "invalid jwks JSON in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("testdata/good-jwk.json", "testdata/not-json.txt"),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "public jwk in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("testdata/public-jwk.json", "testdata/good-jwks.json"),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "private jwks in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("testdata/good-jwk.json", "testdata/private-jwks.json"),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "invalid jwk key in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("testdata/invalid-key-jwk.json", "testdata/good-jwks.json"),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "invalid jwks key in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("testdata/good-jwk.json", "testdata/invalid-key-jwks.json"),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "missing active jwks in secret",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("testdata/good-jwk.json", "testdata/missing-active-jwks.json"),
|
|
|
|
},
|
|
|
|
wantGenerateKeyCount: 1,
|
|
|
|
wantSecretActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(secretGVR, namespace, goodSecret.Name),
|
|
|
|
kubetesting.NewUpdateAction(secretGVR, namespace, goodSecret),
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantFederationDomainActions: []kubetesting.Action{
|
|
|
|
kubetesting.NewGetAction(federationDomainGVR, namespace, goodFederationDomain.Name),
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "generate key fails",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomainWithStatus,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
generateKeyErr: errors.New("some generate error"),
|
|
|
|
wantError: "cannot generate secret: cannot generate key: some generate error",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "get secret fails",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomain,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
configKubeClient: func(client *kubernetesfake.Clientset) {
|
|
|
|
client.PrependReactor("get", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
|
|
|
return true, nil, errors.New("some get error")
|
|
|
|
})
|
|
|
|
},
|
|
|
|
wantError: "cannot create or update secret: cannot get secret: some get error",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "create secret fails",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomain,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
configKubeClient: func(client *kubernetesfake.Clientset) {
|
|
|
|
client.PrependReactor("create", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
|
|
|
return true, nil, errors.New("some create error")
|
|
|
|
})
|
|
|
|
},
|
|
|
|
wantError: "cannot create or update secret: cannot create secret: some create error",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "update secret fails",
|
2020-12-17 19:34:49 +00:00
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomain,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
secrets: []*corev1.Secret{
|
|
|
|
newSecret("", ""),
|
|
|
|
},
|
|
|
|
configKubeClient: func(client *kubernetesfake.Clientset) {
|
|
|
|
client.PrependReactor("update", "secrets", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
|
|
|
return true, nil, errors.New("some update error")
|
|
|
|
})
|
|
|
|
},
|
|
|
|
wantError: "cannot create or update secret: some update error",
|
|
|
|
},
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
name: "get FederationDomain fails",
|
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomain,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
configPinnipedClient: func(client *pinnipedfake.Clientset) {
|
2020-12-16 22:27:09 +00:00
|
|
|
client.PrependReactor("get", "federationdomains", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
2020-10-14 20:41:16 +00:00
|
|
|
return true, nil, errors.New("some get error")
|
|
|
|
})
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantError: "cannot update FederationDomain: cannot get FederationDomain: some get error",
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
{
|
2020-12-17 19:34:49 +00:00
|
|
|
name: "update federationDomain fails",
|
|
|
|
key: controllerlib.Key{Namespace: goodFederationDomain.Namespace, Name: goodFederationDomain.Name},
|
|
|
|
federationDomains: []*configv1alpha1.FederationDomain{
|
|
|
|
goodFederationDomain,
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
configPinnipedClient: func(client *pinnipedfake.Clientset) {
|
2020-12-16 22:27:09 +00:00
|
|
|
client.PrependReactor("update", "federationdomains", func(_ kubetesting.Action) (bool, runtime.Object, error) {
|
2020-10-14 20:41:16 +00:00
|
|
|
return true, nil, errors.New("some update error")
|
|
|
|
})
|
|
|
|
},
|
2020-12-17 19:34:49 +00:00
|
|
|
wantError: "cannot update FederationDomain: some update error",
|
2020-10-14 20:41:16 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, test := range tests {
|
|
|
|
test := test
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
// We shouldn't run this test in parallel since it messes with a global function (generateKey).
|
|
|
|
generateKeyCount := 0
|
2020-10-15 15:33:08 +00:00
|
|
|
generateKey = func(_ io.Reader) (interface{}, error) {
|
2020-10-14 20:41:16 +00:00
|
|
|
generateKeyCount++
|
2020-10-15 15:33:08 +00:00
|
|
|
return goodKey, test.generateKeyErr
|
2020-10-14 20:41:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
kubeAPIClient := kubernetesfake.NewSimpleClientset()
|
|
|
|
kubeInformerClient := kubernetesfake.NewSimpleClientset()
|
|
|
|
for _, secret := range test.secrets {
|
|
|
|
require.NoError(t, kubeAPIClient.Tracker().Add(secret))
|
|
|
|
require.NoError(t, kubeInformerClient.Tracker().Add(secret))
|
|
|
|
}
|
|
|
|
if test.configKubeClient != nil {
|
|
|
|
test.configKubeClient(kubeAPIClient)
|
|
|
|
}
|
|
|
|
|
|
|
|
pinnipedAPIClient := pinnipedfake.NewSimpleClientset()
|
|
|
|
pinnipedInformerClient := pinnipedfake.NewSimpleClientset()
|
2020-12-17 19:34:49 +00:00
|
|
|
for _, federationDomain := range test.federationDomains {
|
|
|
|
require.NoError(t, pinnipedAPIClient.Tracker().Add(federationDomain))
|
|
|
|
require.NoError(t, pinnipedInformerClient.Tracker().Add(federationDomain))
|
2020-10-14 20:41:16 +00:00
|
|
|
}
|
|
|
|
if test.configPinnipedClient != nil {
|
|
|
|
test.configPinnipedClient(pinnipedAPIClient)
|
|
|
|
}
|
|
|
|
|
|
|
|
kubeInformers := kubeinformers.NewSharedInformerFactory(
|
|
|
|
kubeInformerClient,
|
|
|
|
0,
|
|
|
|
)
|
|
|
|
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(
|
|
|
|
pinnipedInformerClient,
|
|
|
|
0,
|
|
|
|
)
|
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
c := NewJWKSWriterController(
|
2020-10-15 19:40:56 +00:00
|
|
|
map[string]string{
|
|
|
|
"myLabelKey1": "myLabelValue1",
|
|
|
|
"myLabelKey2": "myLabelValue2",
|
|
|
|
},
|
2020-10-14 20:41:16 +00:00
|
|
|
kubeAPIClient,
|
|
|
|
pinnipedAPIClient,
|
|
|
|
kubeInformers.Core().V1().Secrets(),
|
2020-12-16 22:27:09 +00:00
|
|
|
pinnipedInformers.Config().V1alpha1().FederationDomains(),
|
2020-10-14 20:41:16 +00:00
|
|
|
controllerlib.WithInformer,
|
|
|
|
)
|
|
|
|
|
|
|
|
// Must start informers before calling TestRunSynchronously().
|
|
|
|
kubeInformers.Start(ctx.Done())
|
|
|
|
pinnipedInformers.Start(ctx.Done())
|
|
|
|
controllerlib.TestRunSynchronously(t, c)
|
|
|
|
|
|
|
|
err := controllerlib.TestSync(t, c, controllerlib.Context{
|
|
|
|
Context: ctx,
|
|
|
|
Key: test.key,
|
|
|
|
})
|
|
|
|
if test.wantError != "" {
|
|
|
|
require.EqualError(t, err, test.wantError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.Equal(t, test.wantGenerateKeyCount, generateKeyCount)
|
|
|
|
|
|
|
|
if test.wantSecretActions != nil {
|
|
|
|
require.Equal(t, test.wantSecretActions, kubeAPIClient.Actions())
|
|
|
|
}
|
2020-12-17 19:34:49 +00:00
|
|
|
if test.wantFederationDomainActions != nil {
|
|
|
|
require.Equal(t, test.wantFederationDomainActions, pinnipedAPIClient.Actions())
|
2020-10-14 20:41:16 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-17 00:51:40 +00:00
|
|
|
func readJWKJSON(t *testing.T, path string) []byte {
|
2020-10-14 20:41:16 +00:00
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
data, err := ioutil.ReadFile(path)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Trim whitespace from our testdata so that we match the compact JSON encoding of
|
|
|
|
// our implementation.
|
|
|
|
data = bytes.ReplaceAll(data, []byte(" "), []byte{})
|
|
|
|
data = bytes.ReplaceAll(data, []byte("\n"), []byte{})
|
|
|
|
|
|
|
|
return data
|
2020-10-14 13:47:34 +00:00
|
|
|
}
|
2020-10-14 20:41:16 +00:00
|
|
|
|
|
|
|
func boolPtr(b bool) *bool { return &b }
|