package servingcert
import (
"fmt"
"io/ioutil"
"reflect"
"testing"
"time"
kapi "k8s.io/kubernetes/pkg/api"
kapierrors "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
"k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/watch"
"github.com/openshift/origin/pkg/cmd/server/admin"
)
func controllerSetup(startingObjects []runtime.Object, stopChannel chan struct{}, t *testing.T) ( /*caName*/ string, *fake.Clientset, *watch.FakeWatcher, *ServiceServingCertController) {
certDir, err := ioutil.TempDir("", "serving-cert-unit-")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
caInfo := admin.DefaultServiceSignerCAInfo(certDir)
caOptions := admin.CreateSignerCertOptions{
CertFile: caInfo.CertFile,
KeyFile: caInfo.KeyFile,
Name: admin.DefaultServiceServingCertSignerName(),
Output: ioutil.Discard,
}
ca, err := caOptions.CreateSignerCert()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
kubeclient := fake.NewSimpleClientset(startingObjects...)
fakeWatch := watch.NewFake()
kubeclient.PrependReactor("create", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, action.(core.CreateAction).GetObject(), nil
})
kubeclient.PrependReactor("update", "*", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, action.(core.UpdateAction).GetObject(), nil
})
kubeclient.PrependWatchReactor("*", core.DefaultWatchReactor(fakeWatch, nil))
controller := NewServiceServingCertController(kubeclient.Core(), kubeclient.Core(), ca, "cluster.local", 10*time.Minute)
return caOptions.Name, kubeclient, fakeWatch, controller
}
func TestBasicControllerFlow(t *testing.T) {
stopChannel := make(chan struct{})
defer close(stopChannel)
received := make(chan bool)
caName, kubeclient, fakeWatch, controller := controllerSetup([]runtime.Object{}, stopChannel, t)
controller.syncHandler = func(serviceKey string) error {
defer func() { received <- true }()
err := controller.syncService(serviceKey)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return err
}
go controller.Run(1, stopChannel)
expectedSecretName := "new-secret"
serviceName := "svc-name"
serviceUID := "some-uid"
expectedServiceAnnotations := map[string]string{ServingCertSecretAnnotation: expectedSecretName, ServingCertCreatedByAnnotation: caName}
expectedSecretAnnotations := map[string]string{ServiceUIDAnnotation: serviceUID, ServiceNameAnnotation: serviceName}
namespace := "ns"
serviceToAdd := &kapi.Service{}
serviceToAdd.Name = serviceName
serviceToAdd.Namespace = namespace
serviceToAdd.UID = types.UID(serviceUID)
serviceToAdd.Annotations = map[string]string{ServingCertSecretAnnotation: expectedSecretName}
fakeWatch.Add(serviceToAdd)
t.Log("waiting to reach syncHandler")
select {
case <-received:
case <-time.After(time.Duration(30 * time.Second)):
t.Fatalf("failed to call into syncService")
}
foundSecret := false
foundServiceUpdate := false
for _, action := range kubeclient.Actions() {
switch {
case action.Matches("create", "secrets"):
createSecret := action.(core.CreateAction)
newSecret := createSecret.GetObject().(*kapi.Secret)
if newSecret.Name != expectedSecretName {
t.Errorf("expected %v, got %v", expectedSecretName, newSecret.Name)
continue
}
if newSecret.Namespace != namespace {
t.Errorf("expected %v, got %v", namespace, newSecret.Namespace)
continue
}
delete(newSecret.Annotations, ServingCertExpiryAnnotation)
if !reflect.DeepEqual(newSecret.Annotations, expectedSecretAnnotations) {
t.Errorf("expected %v, got %v", expectedSecretAnnotations, newSecret.Annotations)
continue
}
foundSecret = true
case action.Matches("update", "services"):
updateService := action.(core.UpdateAction)
service := updateService.GetObject().(*kapi.Service)
if !reflect.DeepEqual(service.Annotations, expectedServiceAnnotations) {
t.Errorf("expected %v, got %v", expectedServiceAnnotations, service.Annotations)
continue
}
foundServiceUpdate = true
}
}
if !foundSecret {
t.Errorf("secret wasn't created. Got %v\n", kubeclient.Actions())
}
if !foundServiceUpdate {
t.Errorf("service wasn't updated. Got %v\n", kubeclient.Actions())
}
}
func TestAlreadyExistingSecretControllerFlow(t *testing.T) {
stopChannel := make(chan struct{})
defer close(stopChannel)
received := make(chan bool)
expectedSecretName := "new-secret"
serviceName := "svc-name"
serviceUID := "some-uid"
expectedSecretAnnotations := map[string]string{ServiceUIDAnnotation: serviceUID, ServiceNameAnnotation: serviceName}
namespace := "ns"
existingSecret := &kapi.Secret{}
existingSecret.Name = expectedSecretName
existingSecret.Namespace = namespace
existingSecret.Type = kapi.SecretTypeTLS
existingSecret.Annotations = expectedSecretAnnotations
caName, kubeclient, fakeWatch, controller := controllerSetup([]runtime.Object{existingSecret}, stopChannel, t)
kubeclient.PrependReactor("create", "secrets", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, &kapi.Secret{}, kapierrors.NewAlreadyExists(kapi.Resource("secrets"), "new-secret")
})
controller.syncHandler = func(serviceKey string) error {
defer func() { received <- true }()
err := controller.syncService(serviceKey)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return err
}
go controller.Run(1, stopChannel)
expectedServiceAnnotations := map[string]string{ServingCertSecretAnnotation: expectedSecretName, ServingCertCreatedByAnnotation: caName}
serviceToAdd := &kapi.Service{}
serviceToAdd.Name = serviceName
serviceToAdd.Namespace = namespace
serviceToAdd.UID = types.UID(serviceUID)
serviceToAdd.Annotations = map[string]string{ServingCertSecretAnnotation: expectedSecretName}
fakeWatch.Add(serviceToAdd)
t.Log("waiting to reach syncHandler")
select {
case <-received:
case <-time.After(time.Duration(30 * time.Second)):
t.Fatalf("failed to call into syncService")
}
foundSecret := false
foundServiceUpdate := false
for _, action := range kubeclient.Actions() {
switch {
case action.Matches("get", "secrets"):
foundSecret = true
case action.Matches("update", "services"):
updateService := action.(core.UpdateAction)
service := updateService.GetObject().(*kapi.Service)
if !reflect.DeepEqual(service.Annotations, expectedServiceAnnotations) {
t.Errorf("expected %v, got %v", expectedServiceAnnotations, service.Annotations)
continue
}
foundServiceUpdate = true
}
}
if !foundSecret {
t.Errorf("secret wasn't retrieved. Got %v\n", kubeclient.Actions())
}
if !foundServiceUpdate {
t.Errorf("service wasn't updated. Got %v\n", kubeclient.Actions())
}
}
func TestAlreadyExistingSecretForDifferentUIDControllerFlow(t *testing.T) {
stopChannel := make(chan struct{})
defer close(stopChannel)
received := make(chan bool)
expectedError := "secret/new-secret references serviceUID wrong-uid, which does not match some-uid"
expectedSecretName := "new-secret"
serviceName := "svc-name"
serviceUID := "some-uid"
namespace := "ns"
existingSecret := &kapi.Secret{}
existingSecret.Name = expectedSecretName
existingSecret.Namespace = namespace
existingSecret.Type = kapi.SecretTypeTLS
existingSecret.Annotations = map[string]string{ServiceUIDAnnotation: "wrong-uid", ServiceNameAnnotation: serviceName}
_, kubeclient, fakeWatch, controller := controllerSetup([]runtime.Object{existingSecret}, stopChannel, t)
kubeclient.PrependReactor("create", "secrets", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, &kapi.Secret{}, kapierrors.NewAlreadyExists(kapi.Resource("secrets"), "new-secret")
})
controller.syncHandler = func(serviceKey string) error {
defer func() { received <- true }()
err := controller.syncService(serviceKey)
if err != nil && err.Error() != expectedError {
t.Errorf("unexpected error: %v", err)
}
return err
}
go controller.Run(1, stopChannel)
expectedServiceAnnotations := map[string]string{ServingCertSecretAnnotation: expectedSecretName, ServingCertErrorAnnotation: expectedError, ServingCertErrorNumAnnotation: "1"}
serviceToAdd := &kapi.Service{}
serviceToAdd.Name = serviceName
serviceToAdd.Namespace = namespace
serviceToAdd.UID = types.UID(serviceUID)
serviceToAdd.Annotations = map[string]string{ServingCertSecretAnnotation: expectedSecretName}
fakeWatch.Add(serviceToAdd)
t.Log("waiting to reach syncHandler")
select {
case <-received:
case <-time.After(time.Duration(30 * time.Second)):
t.Fatalf("failed to call into syncService")
}
foundSecret := false
foundServiceUpdate := false
for _, action := range kubeclient.Actions() {
switch {
case action.Matches("get", "secrets"):
foundSecret = true
case action.Matches("update", "services"):
updateService := action.(core.UpdateAction)
service := updateService.GetObject().(*kapi.Service)
if !reflect.DeepEqual(service.Annotations, expectedServiceAnnotations) {
t.Errorf("expected %v, got %v", expectedServiceAnnotations, service.Annotations)
continue
}
foundServiceUpdate = true
}
}
if !foundSecret {
t.Errorf("secret wasn't retrieved. Got %v\n", kubeclient.Actions())
}
if !foundServiceUpdate {
t.Errorf("service wasn't updated. Got %v\n", kubeclient.Actions())
}
}
func TestSecretCreationErrorControllerFlow(t *testing.T) {
stopChannel := make(chan struct{})
defer close(stopChannel)
received := make(chan bool)
expectedError := `secrets "new-secret" is forbidden: any reason`
expectedSecretName := "new-secret"
serviceName := "svc-name"
serviceUID := "some-uid"
namespace := "ns"
_, kubeclient, fakeWatch, controller := controllerSetup([]runtime.Object{}, stopChannel, t)
kubeclient.PrependReactor("create", "secrets", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, &kapi.Secret{}, kapierrors.NewForbidden(kapi.Resource("secrets"), "new-secret", fmt.Errorf("any reason"))
})
controller.syncHandler = func(serviceKey string) error {
defer func() { received <- true }()
err := controller.syncService(serviceKey)
if err != nil && err.Error() != expectedError {
t.Errorf("unexpected error: %v", err)
}
return err
}
go controller.Run(1, stopChannel)
expectedServiceAnnotations := map[string]string{ServingCertSecretAnnotation: expectedSecretName, ServingCertErrorAnnotation: expectedError, ServingCertErrorNumAnnotation: "1"}
serviceToAdd := &kapi.Service{}
serviceToAdd.Name = serviceName
serviceToAdd.Namespace = namespace
serviceToAdd.UID = types.UID(serviceUID)
serviceToAdd.Annotations = map[string]string{ServingCertSecretAnnotation: expectedSecretName}
fakeWatch.Add(serviceToAdd)
t.Log("waiting to reach syncHandler")
select {
case <-received:
case <-time.After(time.Duration(30 * time.Second)):
t.Fatalf("failed to call into syncService")
}
foundServiceUpdate := false
for _, action := range kubeclient.Actions() {
switch {
case action.Matches("update", "services"):
updateService := action.(core.UpdateAction)
service := updateService.GetObject().(*kapi.Service)
if !reflect.DeepEqual(service.Annotations, expectedServiceAnnotations) {
t.Errorf("expected %v, got %v", expectedServiceAnnotations, service.Annotations)
continue
}
foundServiceUpdate = true
}
}
if !foundServiceUpdate {
t.Errorf("service wasn't updated. Got %v\n", kubeclient.Actions())
}
}
func TestSkipGenerationControllerFlow(t *testing.T) {
stopChannel := make(chan struct{})
defer close(stopChannel)
received := make(chan bool)
expectedSecretName := "new-secret"
serviceName := "svc-name"
serviceUID := "some-uid"
namespace := "ns"
caName, kubeclient, fakeWatch, controller := controllerSetup([]runtime.Object{}, stopChannel, t)
kubeclient.PrependReactor("update", "service", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, &kapi.Service{}, kapierrors.NewForbidden(kapi.Resource("fdsa"), "new-service", fmt.Errorf("any service reason"))
})
kubeclient.PrependReactor("create", "secret", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, &kapi.Secret{}, kapierrors.NewForbidden(kapi.Resource("asdf"), "new-secret", fmt.Errorf("any reason"))
})
kubeclient.PrependReactor("update", "secret", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, &kapi.Secret{}, kapierrors.NewForbidden(kapi.Resource("asdf"), "new-secret", fmt.Errorf("any reason"))
})
controller.syncHandler = func(serviceKey string) error {
defer func() { received <- true }()
err := controller.syncService(serviceKey)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
return err
}
go controller.Run(1, stopChannel)
serviceToAdd := &kapi.Service{}
serviceToAdd.Name = serviceName
serviceToAdd.Namespace = namespace
serviceToAdd.UID = types.UID(serviceUID)
serviceToAdd.Annotations = map[string]string{ServingCertSecretAnnotation: expectedSecretName, ServingCertErrorAnnotation: "any-error", ServingCertErrorNumAnnotation: "11"}
fakeWatch.Add(serviceToAdd)
t.Log("waiting to reach syncHandler")
select {
case <-received:
case <-time.After(time.Duration(30 * time.Second)):
t.Fatalf("failed to call into syncService")
}
for _, action := range kubeclient.Actions() {
switch action.GetVerb() {
case "update", "create":
t.Errorf("no mutation expected, but we got %v", action)
}
}
kubeclient.ClearActions()
serviceToAdd.Annotations = map[string]string{ServingCertSecretAnnotation: expectedSecretName, ServingCertCreatedByAnnotation: caName}
fakeWatch.Add(serviceToAdd)
t.Log("waiting to reach syncHandler")
select {
case <-received:
case <-time.After(time.Duration(30 * time.Second)):
t.Fatalf("failed to call into syncService")
}
for _, action := range kubeclient.Actions() {
switch action.GetVerb() {
case "update", "create":
t.Errorf("no mutation expected, but we got %v", action)
}
}
}