Merge pull request #33 from cfryanr/discovery_doc

Adding discovery document object
This commit is contained in:
Ryan Richard
2020-08-04 10:01:20 -07:00
committed by GitHub
21 changed files with 1143 additions and 128 deletions

View File

@@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
package apiserver
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -56,8 +57,9 @@ type Config struct {
}
type ExtraConfig struct {
Webhook authenticator.Token
Issuer loginrequest.CertIssuer
Webhook authenticator.Token
Issuer loginrequest.CertIssuer
StartControllersPostStartHook func(ctx context.Context)
}
type PlaceHolderServer struct {
@@ -122,9 +124,17 @@ func (c completedConfig) New() (*PlaceHolderServer, error) {
return nil, fmt.Errorf("install API group error: %w", err)
}
s.GenericAPIServer.AddPostStartHookOrDie("place-holder-post-start-hook",
func(context genericapiserver.PostStartHookContext) error {
klog.InfoS("post start hook", "foo", "bar")
s.GenericAPIServer.AddPostStartHookOrDie("start-controllers",
func(postStartContext genericapiserver.PostStartHookContext) error {
klog.InfoS("start-controllers post start hook starting")
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-postStartContext.StopCh
cancel()
}()
c.ExtraConfig.StartControllersPostStartHook(ctx)
return nil
},
)

View File

@@ -0,0 +1,7 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
// Package logindiscovery contains controller(s) for reconciling LoginDiscoveryConfig's.
package logindiscovery

View File

@@ -0,0 +1,192 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package logindiscovery
import (
"context"
"encoding/base64"
"fmt"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"
"github.com/suzerain-io/controller-go"
crdsplaceholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/crdsplaceholder/v1alpha1"
placeholderclientset "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/clientset/versioned"
crdsplaceholderv1alpha1informers "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/informers/externalversions/crdsplaceholder/v1alpha1"
)
const (
ClusterInfoNamespace = "kube-public"
clusterInfoName = "cluster-info"
clusterInfoConfigMapKey = "kubeconfig"
configName = "placeholder-name-config"
)
func nameAndNamespaceExactMatchFilterFactory(name, namespace string) controller.FilterFuncs {
objMatchesFunc := func(obj metav1.Object) bool {
return obj.GetName() == name && obj.GetNamespace() == namespace
}
return controller.FilterFuncs{
AddFunc: objMatchesFunc,
UpdateFunc: func(oldObj, newObj metav1.Object) bool {
return objMatchesFunc(oldObj) || objMatchesFunc(newObj)
},
DeleteFunc: objMatchesFunc,
}
}
// Same signature as controller.WithInformer().
type withInformerOptionFunc func(
getter controller.InformerGetter,
filter controller.Filter,
opt controller.InformerOption) controller.Option
type publisherController struct {
namespace string
serverOverride *string
placeholderClient placeholderclientset.Interface
configMapInformer corev1informers.ConfigMapInformer
loginDiscoveryConfigInformer crdsplaceholderv1alpha1informers.LoginDiscoveryConfigInformer
}
func NewPublisherController(
namespace string,
serverOverride *string,
placeholderClient placeholderclientset.Interface,
configMapInformer corev1informers.ConfigMapInformer,
loginDiscoveryConfigInformer crdsplaceholderv1alpha1informers.LoginDiscoveryConfigInformer,
withInformer withInformerOptionFunc,
) controller.Controller {
return controller.New(
controller.Config{
Name: "publisher-controller",
Syncer: &publisherController{
namespace: namespace,
serverOverride: serverOverride,
placeholderClient: placeholderClient,
configMapInformer: configMapInformer,
loginDiscoveryConfigInformer: loginDiscoveryConfigInformer,
},
},
withInformer(
configMapInformer,
nameAndNamespaceExactMatchFilterFactory(clusterInfoName, ClusterInfoNamespace),
controller.InformerOption{},
),
withInformer(
loginDiscoveryConfigInformer,
nameAndNamespaceExactMatchFilterFactory(configName, namespace),
controller.InformerOption{},
),
)
}
func (c *publisherController) Sync(ctx controller.Context) error {
configMap, err := c.configMapInformer.
Lister().
ConfigMaps(ClusterInfoNamespace).
Get(clusterInfoName)
notFound := k8serrors.IsNotFound(err)
if err != nil && !notFound {
return fmt.Errorf("failed to get %s configmap: %w", clusterInfoName, err)
}
if notFound {
klog.InfoS(
"could not find config map",
"configmap",
klog.KRef(ClusterInfoNamespace, clusterInfoName),
)
return nil
}
kubeConfig, kubeConfigPresent := configMap.Data[clusterInfoConfigMapKey]
if !kubeConfigPresent {
klog.InfoS("could not find kubeconfig configmap key")
return nil
}
config, _ := clientcmd.Load([]byte(kubeConfig))
var certificateAuthorityData, server string
for _, v := range config.Clusters {
certificateAuthorityData = base64.StdEncoding.EncodeToString(v.CertificateAuthorityData)
server = v.Server
break
}
if c.serverOverride != nil {
server = *c.serverOverride
}
discoveryConfig := crdsplaceholderv1alpha1.LoginDiscoveryConfig{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: configName,
Namespace: c.namespace,
},
Spec: crdsplaceholderv1alpha1.LoginDiscoveryConfigSpec{
Server: server,
CertificateAuthorityData: certificateAuthorityData,
},
}
if err := c.createOrUpdateLoginDiscoveryConfig(ctx.Context, &discoveryConfig); err != nil {
return err
}
return nil
}
func (c *publisherController) createOrUpdateLoginDiscoveryConfig(
ctx context.Context,
discoveryConfig *crdsplaceholderv1alpha1.LoginDiscoveryConfig,
) error {
existingDiscoveryConfig, err := c.loginDiscoveryConfigInformer.
Lister().
LoginDiscoveryConfigs(c.namespace).
Get(discoveryConfig.Name)
notFound := k8serrors.IsNotFound(err)
if err != nil && !notFound {
return fmt.Errorf("could not get logindiscoveryconfig: %w", err)
}
loginDiscoveryConfigs := c.placeholderClient.
CrdsV1alpha1().
LoginDiscoveryConfigs(c.namespace)
if notFound {
if _, err := loginDiscoveryConfigs.Create(
ctx,
discoveryConfig,
metav1.CreateOptions{},
); err != nil {
return fmt.Errorf("could not create logindiscoveryconfig: %w", err)
}
} else if !equal(existingDiscoveryConfig, discoveryConfig) {
// Update just the fields we care about.
existingDiscoveryConfig.Spec.Server = discoveryConfig.Spec.Server
existingDiscoveryConfig.Spec.CertificateAuthorityData = discoveryConfig.Spec.CertificateAuthorityData
if _, err := loginDiscoveryConfigs.Update(
ctx,
existingDiscoveryConfig,
metav1.UpdateOptions{},
); err != nil {
return fmt.Errorf("could not update logindiscoveryconfig: %w", err)
}
}
return nil
}
func equal(a, b *crdsplaceholderv1alpha1.LoginDiscoveryConfig) bool {
return a.Spec.Server == b.Spec.Server &&
a.Spec.CertificateAuthorityData == b.Spec.CertificateAuthorityData
}

View File

@@ -0,0 +1,475 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package logindiscovery
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"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"
coretesting "k8s.io/client-go/testing"
"github.com/suzerain-io/controller-go"
crdsplaceholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/crdsplaceholder/v1alpha1"
placeholderfake "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/clientset/versioned/fake"
placeholderinformers "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/informers/externalversions"
)
type ObservableWithInformerOption struct {
InformerToFilterMap map[controller.InformerGetter]controller.Filter
}
func NewObservableWithInformerOption() *ObservableWithInformerOption {
return &ObservableWithInformerOption{
InformerToFilterMap: make(map[controller.InformerGetter]controller.Filter),
}
}
func (owi *ObservableWithInformerOption) WithInformer(
getter controller.InformerGetter,
filter controller.Filter,
opt controller.InformerOption) controller.Option {
owi.InformerToFilterMap[getter] = filter
return controller.WithInformer(getter, filter, opt)
}
func TestInformerFilters(t *testing.T) {
spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) {
const installedInNamespace = "some-namespace"
var r *require.Assertions
var observableWithInformerOption *ObservableWithInformerOption
var configMapInformerFilter controller.Filter
var loginDiscoveryConfigInformerFilter controller.Filter
it.Before(func() {
r = require.New(t)
observableWithInformerOption = NewObservableWithInformerOption()
configMapInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().ConfigMaps()
loginDiscoveryConfigInformer := placeholderinformers.NewSharedInformerFactory(nil, 0).Crds().V1alpha1().LoginDiscoveryConfigs()
_ = NewPublisherController(
installedInNamespace,
nil,
nil,
configMapInformer,
loginDiscoveryConfigInformer,
observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters
)
configMapInformerFilter = observableWithInformerOption.InformerToFilterMap[configMapInformer]
loginDiscoveryConfigInformerFilter = observableWithInformerOption.InformerToFilterMap[loginDiscoveryConfigInformer]
})
when("watching ConfigMap objects", func() {
var subject controller.Filter
var target, wrongNamespace, wrongName, unrelated *corev1.ConfigMap
it.Before(func() {
subject = configMapInformerFilter
target = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cluster-info", Namespace: "kube-public"}}
wrongNamespace = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "cluster-info", Namespace: "wrong-namespace"}}
wrongName = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "kube-public"}}
unrelated = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "wrong-namespace"}}
})
when("the target ConfigMap changes", func() {
it("returns true to trigger the sync method", func() {
r.True(subject.Add(target))
r.True(subject.Update(target, unrelated))
r.True(subject.Update(unrelated, target))
r.True(subject.Delete(target))
})
})
when("a ConfigMap from another namespace changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(wrongNamespace))
r.False(subject.Update(wrongNamespace, unrelated))
r.False(subject.Update(unrelated, wrongNamespace))
r.False(subject.Delete(wrongNamespace))
})
})
when("a ConfigMap with a different name changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(wrongName))
r.False(subject.Update(wrongName, unrelated))
r.False(subject.Update(unrelated, wrongName))
r.False(subject.Delete(wrongName))
})
})
when("a ConfigMap with a different name and a different namespace changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(unrelated))
r.False(subject.Update(unrelated, unrelated))
r.False(subject.Delete(unrelated))
})
})
})
when("watching LoginDiscoveryConfig objects", func() {
var subject controller.Filter
var target, wrongNamespace, wrongName, unrelated *crdsplaceholderv1alpha1.LoginDiscoveryConfig
it.Before(func() {
subject = loginDiscoveryConfigInformerFilter
target = &crdsplaceholderv1alpha1.LoginDiscoveryConfig{
ObjectMeta: metav1.ObjectMeta{Name: "placeholder-name-config", Namespace: installedInNamespace},
}
wrongNamespace = &crdsplaceholderv1alpha1.LoginDiscoveryConfig{
ObjectMeta: metav1.ObjectMeta{Name: "placeholder-name-config", Namespace: "wrong-namespace"},
}
wrongName = &crdsplaceholderv1alpha1.LoginDiscoveryConfig{
ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: installedInNamespace},
}
unrelated = &crdsplaceholderv1alpha1.LoginDiscoveryConfig{
ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "wrong-namespace"},
}
})
when("the target LoginDiscoveryConfig changes", func() {
it("returns true to trigger the sync method", func() {
r.True(subject.Add(target))
r.True(subject.Update(target, unrelated))
r.True(subject.Update(unrelated, target))
r.True(subject.Delete(target))
})
})
when("a LoginDiscoveryConfig from another namespace changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(wrongNamespace))
r.False(subject.Update(wrongNamespace, unrelated))
r.False(subject.Update(unrelated, wrongNamespace))
r.False(subject.Delete(wrongNamespace))
})
})
when("a LoginDiscoveryConfig with a different name changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(wrongName))
r.False(subject.Update(wrongName, unrelated))
r.False(subject.Update(unrelated, wrongName))
r.False(subject.Delete(wrongName))
})
})
when("a LoginDiscoveryConfig with a different name and a different namespace changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(unrelated))
r.False(subject.Update(unrelated, unrelated))
r.False(subject.Delete(unrelated))
})
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}
func TestSync(t *testing.T) {
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
const installedInNamespace = "some-namespace"
var r *require.Assertions
var subject controller.Controller
var serverOverride *string
var kubeInformerClient *kubernetesfake.Clientset
var placeholderInformerClient *placeholderfake.Clientset
var kubeInformers kubeinformers.SharedInformerFactory
var placeholderInformers placeholderinformers.SharedInformerFactory
var placeholderAPIClient *placeholderfake.Clientset
var timeoutContext context.Context
var timeoutContextCancel context.CancelFunc
var syncContext *controller.Context
var expectedLoginDiscoveryConfig = func(expectedNamespace, expectedServerURL, expectedCAData string) (schema.GroupVersionResource, *crdsplaceholderv1alpha1.LoginDiscoveryConfig) {
expectedLoginDiscoveryConfigGVR := schema.GroupVersionResource{
Group: crdsplaceholderv1alpha1.GroupName,
Version: "v1alpha1",
Resource: "logindiscoveryconfigs",
}
expectedLoginDiscoveryConfig := &crdsplaceholderv1alpha1.LoginDiscoveryConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "placeholder-name-config",
Namespace: expectedNamespace,
},
Spec: crdsplaceholderv1alpha1.LoginDiscoveryConfigSpec{
Server: expectedServerURL,
CertificateAuthorityData: expectedCAData,
},
}
return expectedLoginDiscoveryConfigGVR, expectedLoginDiscoveryConfig
}
// Defer starting the informers until the last possible moment so that the
// nested Before's can keep adding things to the informer caches.
var startInformersAndController = func() {
// Set this at the last second to allow for injection of server override.
subject = NewPublisherController(
installedInNamespace,
serverOverride,
placeholderAPIClient,
kubeInformers.Core().V1().ConfigMaps(),
placeholderInformers.Crds().V1alpha1().LoginDiscoveryConfigs(),
controller.WithInformer,
)
// Set this at the last second to support calling subject.Name().
syncContext = &controller.Context{
Context: timeoutContext,
Name: subject.Name(),
Key: controller.Key{
Namespace: "kube-public",
Name: "cluster-info",
},
}
// Must start informers before calling TestRunSynchronously()
kubeInformers.Start(timeoutContext.Done())
placeholderInformers.Start(timeoutContext.Done())
controller.TestRunSynchronously(t, subject)
}
it.Before(func() {
r = require.New(t)
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
kubeInformerClient = kubernetesfake.NewSimpleClientset()
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
placeholderAPIClient = placeholderfake.NewSimpleClientset()
placeholderInformerClient = placeholderfake.NewSimpleClientset()
placeholderInformers = placeholderinformers.NewSharedInformerFactory(placeholderInformerClient, 0)
})
it.After(func() {
timeoutContextCancel()
})
when("there is a cluster-info ConfigMap in the kube-public namespace", func() {
const caData = "c29tZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YQo=" // "some-certificate-authority-data" base64 encoded
const kubeServerURL = "https://some-server"
when("the ConfigMap has the expected `kubeconfig` top-level data key", func() {
it.Before(func() {
clusterInfoConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cluster-info", Namespace: "kube-public"},
// Note that go fmt puts tabs in our file, which we must remove from our configmap yaml below.
Data: map[string]string{
"kubeconfig": strings.ReplaceAll(`
kind: Config
apiVersion: v1
clusters:
- name: ""
cluster:
certificate-authority-data: "`+caData+`"
server: "`+kubeServerURL+`"`, "\t", " "),
"uninteresting-key": "uninteresting-value",
},
}
err := kubeInformerClient.Tracker().Add(clusterInfoConfigMap)
r.NoError(err)
})
when("the LoginDiscoveryConfig does not already exist", func() {
it("creates a LoginDiscoveryConfig", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.NoError(err)
expectedLoginDiscoveryConfigGVR, expectedLoginDiscoveryConfig := expectedLoginDiscoveryConfig(
installedInNamespace,
kubeServerURL,
caData,
)
r.Equal(
[]coretesting.Action{
coretesting.NewCreateAction(
expectedLoginDiscoveryConfigGVR,
installedInNamespace,
expectedLoginDiscoveryConfig,
),
},
placeholderAPIClient.Actions(),
)
})
when("creating the LoginDiscoveryConfig fails", func() {
it.Before(func() {
placeholderAPIClient.PrependReactor(
"create",
"logindiscoveryconfigs",
func(_ coretesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("create failed")
},
)
})
it("returns the create error", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.EqualError(err, "could not create logindiscoveryconfig: create failed")
})
})
when("a server override is passed to the controller", func() {
it("uses the server override field", func() {
serverOverride = new(string)
*serverOverride = "https://some-server-override"
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.NoError(err)
expectedLoginDiscoveryConfigGVR, expectedLoginDiscoveryConfig := expectedLoginDiscoveryConfig(
installedInNamespace,
kubeServerURL,
caData,
)
expectedLoginDiscoveryConfig.Spec.Server = "https://some-server-override"
r.Equal(
[]coretesting.Action{
coretesting.NewCreateAction(
expectedLoginDiscoveryConfigGVR,
installedInNamespace,
expectedLoginDiscoveryConfig,
),
},
placeholderAPIClient.Actions(),
)
})
})
})
when("the LoginDiscoveryConfig already exists", func() {
when("the LoginDiscoveryConfig is already up to date according to the data in the ConfigMap", func() {
it.Before(func() {
_, expectedLoginDiscoveryConfig := expectedLoginDiscoveryConfig(
installedInNamespace,
kubeServerURL,
caData,
)
err := placeholderInformerClient.Tracker().Add(expectedLoginDiscoveryConfig)
r.NoError(err)
})
it("does not update the LoginDiscoveryConfig to avoid unnecessary etcd writes/api calls", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.NoError(err)
r.Empty(placeholderAPIClient.Actions())
})
})
when("the LoginDiscoveryConfig is stale compared to the data in the ConfigMap", func() {
it.Before(func() {
_, expectedLoginDiscoveryConfig := expectedLoginDiscoveryConfig(
installedInNamespace,
kubeServerURL,
caData,
)
expectedLoginDiscoveryConfig.Spec.Server = "https://some-other-server"
r.NoError(placeholderInformerClient.Tracker().Add(expectedLoginDiscoveryConfig))
r.NoError(placeholderAPIClient.Tracker().Add(expectedLoginDiscoveryConfig))
})
it("updates the existing LoginDiscoveryConfig", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.NoError(err)
expectedLoginDiscoveryConfigGVR, expectedLoginDiscoveryConfig := expectedLoginDiscoveryConfig(
installedInNamespace,
kubeServerURL,
caData,
)
expectedActions := []coretesting.Action{
coretesting.NewUpdateAction(
expectedLoginDiscoveryConfigGVR,
installedInNamespace,
expectedLoginDiscoveryConfig,
),
}
r.Equal(expectedActions, placeholderAPIClient.Actions())
})
when("updating the LoginDiscoveryConfig fails", func() {
it.Before(func() {
placeholderAPIClient.PrependReactor(
"update",
"logindiscoveryconfigs",
func(_ coretesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("update failed")
},
)
})
it("returns the update error", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.EqualError(err, "could not update logindiscoveryconfig: update failed")
})
})
})
})
})
when("the ConfigMap is missing the expected `kubeconfig` top-level data key", func() {
it.Before(func() {
clusterInfoConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: "cluster-info", Namespace: "kube-public"},
Data: map[string]string{
"these are not the droids you're looking for": "uninteresting-value",
},
}
err := kubeInformerClient.Tracker().Add(clusterInfoConfigMap)
r.NoError(err)
})
it("keeps waiting for it to exist", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.NoError(err)
r.Empty(placeholderAPIClient.Actions())
})
})
})
when("there is not a cluster-info ConfigMap in the kube-public namespace", func() {
it.Before(func() {
unrelatedConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "oops this is not the cluster-info ConfigMap",
Namespace: "kube-public",
},
}
err := kubeInformerClient.Tracker().Add(unrelatedConfigMap)
r.NoError(err)
})
it("keeps waiting for one", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.NoError(err)
r.Empty(placeholderAPIClient.Actions())
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}

View File

@@ -25,20 +25,29 @@ import (
"k8s.io/apiserver/pkg/server/dynamiccertificates"
genericoptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
k8sinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
aggregationv1client "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
"github.com/suzerain-io/controller-go"
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1"
placeholderclientset "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/clientset/versioned"
placeholderinformers "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/informers/externalversions"
"github.com/suzerain-io/placeholder-name/internal/apiserver"
"github.com/suzerain-io/placeholder-name/internal/autoregistration"
"github.com/suzerain-io/placeholder-name/internal/certauthority"
"github.com/suzerain-io/placeholder-name/internal/controller/logindiscovery"
"github.com/suzerain-io/placeholder-name/internal/downward"
"github.com/suzerain-io/placeholder-name/pkg/config"
)
const (
singletonWorker = 1
defaultResyncInterval = 3 * time.Minute
)
// App is an object that represents the placeholder-name-server application.
type App struct {
cmd *cobra.Command
@@ -83,18 +92,26 @@ authenticating to the Kubernetes API.`,
protoKubeConfig := createProtoKubeConfig(kubeConfig)
// Connect to the core Kubernetes API.
k8s, err := kubernetes.NewForConfig(protoKubeConfig)
k8sClient, err := kubernetes.NewForConfig(protoKubeConfig)
if err != nil {
return fmt.Errorf("could not initialize Kubernetes client: %w", err)
}
// Connect to the Kubernetes aggregation API.
aggregation, err := aggregationv1client.NewForConfig(protoKubeConfig)
aggregationClient, err := aggregationv1client.NewForConfig(protoKubeConfig)
if err != nil {
return fmt.Errorf("could not initialize Kubernetes client: %w", err)
}
return a.run(ctx, k8s.CoreV1(), aggregation)
// Connect to the placeholder API.
// I think we can't use protobuf encoding here because we are using CRDs
// (for which protobuf encoding is not supported).
placeholderClient, err := placeholderclientset.NewForConfig(kubeConfig)
if err != nil {
return fmt.Errorf("could not initialize placeholder client: %w", err)
}
return a.run(ctx, k8sClient, aggregationClient, placeholderClient)
},
Args: cobra.NoArgs,
}
@@ -143,8 +160,9 @@ func (a *App) Run() error {
func (a *App) run(
ctx context.Context,
k8s corev1client.CoreV1Interface,
aggregation aggregationv1client.Interface,
k8sClient kubernetes.Interface,
aggregationClient aggregationv1client.Interface,
placeholderClient placeholderclientset.Interface,
) error {
cfg, err := config.FromPath(a.configPath)
if err != nil {
@@ -166,6 +184,7 @@ func (a *App) run(
if err != nil {
return fmt.Errorf("could not read pod metadata: %w", err)
}
serverInstallationNamespace := podinfo.Namespace
// TODO use the postStart hook to generate certs?
@@ -177,7 +196,7 @@ func (a *App) run(
const serviceName = "placeholder-name-api"
cert, err := apiCA.Issue(
pkix.Name{CommonName: serviceName + "." + podinfo.Namespace + ".svc"},
pkix.Name{CommonName: serviceName + "." + serverInstallationNamespace + ".svc"},
[]string{},
24*365*time.Hour,
)
@@ -213,16 +232,27 @@ func (a *App) run(
},
}
if err := autoregistration.Setup(ctx, autoregistration.SetupOptions{
CoreV1: k8s,
AggregationV1: aggregation,
Namespace: podinfo.Namespace,
CoreV1: k8sClient.CoreV1(),
AggregationV1: aggregationClient,
Namespace: serverInstallationNamespace,
ServiceTemplate: service,
APIServiceTemplate: apiService,
}); err != nil {
return fmt.Errorf("could not register API service: %w", err)
}
apiServerConfig, err := a.ConfigServer(cert, webhookTokenAuthenticator, clientCA)
cmrf := wireControllerManagerRunFunc(
serverInstallationNamespace,
cfg.DiscoveryConfig.URL,
k8sClient,
placeholderClient,
)
apiServerConfig, err := a.configServer(
cert,
webhookTokenAuthenticator,
clientCA,
cmrf,
)
if err != nil {
return err
}
@@ -235,7 +265,12 @@ func (a *App) run(
return server.GenericAPIServer.PrepareRun().Run(ctx.Done())
}
func (a *App) ConfigServer(cert *tls.Certificate, webhookTokenAuthenticator *webhook.WebhookTokenAuthenticator, ca *certauthority.CA) (*apiserver.Config, error) {
func (a *App) configServer(
cert *tls.Certificate,
webhookTokenAuthenticator *webhook.WebhookTokenAuthenticator,
ca *certauthority.CA,
startControllersPostStartHook func(context.Context),
) (*apiserver.Config, error) {
provider, err := createStaticCertKeyProvider(cert)
if err != nil {
return nil, fmt.Errorf("could not create static cert key provider: %w", err)
@@ -250,8 +285,9 @@ func (a *App) ConfigServer(cert *tls.Certificate, webhookTokenAuthenticator *web
apiServerConfig := &apiserver.Config{
GenericConfig: serverConfig,
ExtraConfig: apiserver.ExtraConfig{
Webhook: webhookTokenAuthenticator,
Issuer: ca,
Webhook: webhookTokenAuthenticator,
Issuer: ca,
StartControllersPostStartHook: startControllersPostStartHook,
},
}
return apiServerConfig, nil
@@ -290,3 +326,41 @@ func createStaticCertKeyProvider(cert *tls.Certificate) (dynamiccertificates.Cer
return dynamiccertificates.NewStaticCertKeyContent("some-name???", certChainPEM, privateKeyPEM)
}
func wireControllerManagerRunFunc(
serverInstallationNamespace string,
discoveryURLOverride *string,
k8s kubernetes.Interface,
placeholder placeholderclientset.Interface,
) func(ctx context.Context) {
k8sInformers := k8sinformers.NewSharedInformerFactoryWithOptions(
k8s,
defaultResyncInterval,
k8sinformers.WithNamespace(
logindiscovery.ClusterInfoNamespace,
),
)
placeholderInformers := placeholderinformers.NewSharedInformerFactoryWithOptions(
placeholder,
defaultResyncInterval,
placeholderinformers.WithNamespace(serverInstallationNamespace),
)
cm := controller.
NewManager().
WithController(
logindiscovery.NewPublisherController(
serverInstallationNamespace,
discoveryURLOverride,
placeholder,
k8sInformers.Core().V1().ConfigMaps(),
placeholderInformers.Crds().V1alpha1().LoginDiscoveryConfigs(),
controller.WithInformer,
),
singletonWorker,
)
return func(ctx context.Context) {
k8sInformers.Start(ctx.Done())
placeholderInformers.Start(ctx.Done())
go cm.Start(ctx)
}
}