mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-25 16:07:24 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import "github.com/clastix/capsule/pkg/controller/namespace"
|
||||
|
||||
func init() {
|
||||
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
|
||||
AddToManagerFuncs = append(AddToManagerFuncs, namespace.Add)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/clastix/capsule/pkg/controller/secret"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
|
||||
AddToManagerFuncs = append(AddToManagerFuncs, secret.AddTls, secret.AddCa)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/clastix/capsule/pkg/controller/tenant"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// AddToManagerFuncs is a list of functions to create controllers and add them to a manager.
|
||||
AddToManagerFuncs = append(AddToManagerFuncs, tenant.Add)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
)
|
||||
|
||||
// AddToManagerFuncs is a list of functions to add all Controllers to the Manager
|
||||
var AddToManagerFuncs []func(manager.Manager) error
|
||||
|
||||
// AddToManager adds all Controllers to the Manager
|
||||
func AddToManager(m manager.Manager) error {
|
||||
for _, f := range AddToManagerFuncs {
|
||||
if err := f(m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package namespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
"sort"
|
||||
|
||||
"github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
|
||||
)
|
||||
|
||||
// Add creates a new Namespace Controller and adds it to the Manager. The Manager will set fields on the Controller
|
||||
// and Start it when the Manager is Started.
|
||||
func Add(mgr manager.Manager) error {
|
||||
return add(mgr, newReconciler(mgr))
|
||||
}
|
||||
|
||||
// newReconciler returns a new reconcile.Reconciler
|
||||
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
|
||||
return &ReconcileNamespace{
|
||||
client: mgr.GetClient(),
|
||||
scheme: mgr.GetScheme(),
|
||||
}
|
||||
}
|
||||
|
||||
// add adds a new Controller to mgr with r as the reconcile.Reconciler
|
||||
func add(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
// Create a new controller
|
||||
c, err := controller.New("namespace-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to primary resource Namespace
|
||||
err = c.Watch(&source.Kind{Type: &corev1.Namespace{}}, &handler.EnqueueRequestForObject{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReconcileNamespace reconciles a Namespace object
|
||||
type ReconcileNamespace struct {
|
||||
client client.Client
|
||||
scheme *runtime.Scheme
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
func (r *ReconcileNamespace) removeNamespace(name string, tenant *v1alpha1.Tenant) {
|
||||
c := tenant.Status.Namespaces.DeepCopy()
|
||||
sort.Sort(c)
|
||||
i := sort.SearchStrings(c, name)
|
||||
// namespace already removed, do nothing
|
||||
if i > c.Len() || i == c.Len() {
|
||||
return
|
||||
}
|
||||
// namespace is there, removing it
|
||||
tenant.Status.Namespaces = []string{}
|
||||
tenant.Status.Namespaces = append(tenant.Status.Namespaces, c[:i]...)
|
||||
tenant.Status.Namespaces = append(tenant.Status.Namespaces, c[i+1:]...)
|
||||
}
|
||||
|
||||
func (r *ReconcileNamespace) addNamespace(name string, tenant *v1alpha1.Tenant) {
|
||||
c := tenant.Status.Namespaces.DeepCopy()
|
||||
sort.Sort(c)
|
||||
i := sort.SearchStrings(c, name)
|
||||
// namespace already there, nothing to do
|
||||
if i < c.Len() && c[i] == name {
|
||||
return
|
||||
}
|
||||
// missing namespace, let's append it
|
||||
if i == 0 {
|
||||
tenant.Status.Namespaces = []string{name}
|
||||
} else {
|
||||
tenant.Status.Namespaces = v1alpha1.NamespaceList{}
|
||||
tenant.Status.Namespaces = append(tenant.Status.Namespaces, c[:i]...)
|
||||
tenant.Status.Namespaces = append(tenant.Status.Namespaces, name)
|
||||
}
|
||||
tenant.Status.Namespaces = append(tenant.Status.Namespaces, c[i:]...)
|
||||
}
|
||||
|
||||
func (r *ReconcileNamespace) updateNamespaceCount(tenant *v1alpha1.Tenant) error {
|
||||
tenant.Status.Size = uint(len(tenant.Status.Namespaces))
|
||||
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
return r.client.Status().Update(context.TODO(), tenant, &client.UpdateOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func (r *ReconcileNamespace) Reconcile(request reconcile.Request) (res reconcile.Result, err error) {
|
||||
r.logger = log.Log.WithName("controller_namespace").WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
r.logger.Info("Reconciling Namespace")
|
||||
|
||||
// Fetch the Namespace instance
|
||||
ns := &corev1.Namespace{}
|
||||
if err := r.client.Get(context.TODO(), request.NamespacedName, ns); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
// Request object not found, could have been deleted after reconcile request.
|
||||
// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
|
||||
// Return and don't requeue
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
// Error reading the object - requeue the request.
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
// Skipping NS non referenced to a Tenant
|
||||
if len(ns.OwnerReferences) == 0 {
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
t := &v1alpha1.Tenant{}
|
||||
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: ns.OwnerReferences[0].Name}, t); err != nil {
|
||||
// Error reading the object - requeue the request.
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if err := r.ensureLabel(ns, t.Name); err != nil {
|
||||
r.logger.Error(err, "cannot update Namespace label")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.updateTenantStatus(ns, t)
|
||||
|
||||
if err := r.updateNamespaceCount(t); err != nil {
|
||||
r.logger.Error(err, "cannot update Namespace list", "tenant", t.Name)
|
||||
}
|
||||
|
||||
r.logger.Info("Namespace reconciliation processed")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ReconcileNamespace) ensureLabel(ns *corev1.Namespace, tenantName string) error {
|
||||
capsuleLabel, err := v1alpha1.GetTypeLabel(&v1alpha1.Tenant{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ns.Labels == nil {
|
||||
ns.Labels = make(map[string]string)
|
||||
}
|
||||
tl, ok := ns.Labels[capsuleLabel]
|
||||
if !ok || tl != tenantName {
|
||||
ns.Labels[capsuleLabel] = tenantName
|
||||
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
return r.client.Update(context.TODO(), ns, &client.UpdateOptions{})
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReconcileNamespace) updateTenantStatus(ns *corev1.Namespace, tenant *v1alpha1.Tenant) {
|
||||
switch ns.Status.Phase {
|
||||
case corev1.NamespaceTerminating:
|
||||
r.removeNamespace(ns.Name, tenant)
|
||||
case corev1.NamespaceActive:
|
||||
r.addNamespace(ns.Name, tenant)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package secret
|
||||
|
||||
const (
|
||||
Cert = "tls.crt"
|
||||
PrivateKey = "tls.key"
|
||||
|
||||
CaSecretName = "capsule-ca"
|
||||
TlsSecretName = "capsule-tls"
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/clastix/capsule/pkg/cert"
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
type secretReconciliationFunc func(reconciler *ReconcileSecret, request reconcile.Request) (reconcile.Result, error)
|
||||
|
||||
// ReconcileSecret reconciles a Secret object
|
||||
type ReconcileSecret struct {
|
||||
client client.Client
|
||||
scheme *runtime.Scheme
|
||||
logger logr.Logger
|
||||
reconcileFunc secretReconciliationFunc
|
||||
}
|
||||
|
||||
func newReconciler(mgr manager.Manager, name string, f secretReconciliationFunc) reconcile.Reconciler {
|
||||
return &ReconcileSecret{
|
||||
client: mgr.GetClient(),
|
||||
scheme: mgr.GetScheme(),
|
||||
logger: log.Log.WithName(name),
|
||||
reconcileFunc: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReconcileSecret) Reconcile(request reconcile.Request) (reconcile.Result, error) {
|
||||
return r.reconcileFunc(r, request)
|
||||
}
|
||||
|
||||
func (r *ReconcileSecret) GetCertificateAuthority() (ca cert.Ca, err error) {
|
||||
instance := &corev1.Secret{}
|
||||
|
||||
err = r.client.Get(context.TODO(), types.NamespacedName{
|
||||
Namespace: "capsule-system",
|
||||
Name: CaSecretName,
|
||||
}, instance)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("missing secret %s, cannot reconcile", CaSecretName)
|
||||
}
|
||||
|
||||
if instance.Data == nil {
|
||||
ca, err = cert.GenerateCertificateAuthority()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
instance.Data = map[string][]byte{}
|
||||
|
||||
crt, _ := ca.CaCertificatePem()
|
||||
instance.Data[Cert] = crt.Bytes()
|
||||
key, _ := ca.CaPrivateKeyPem()
|
||||
instance.Data[PrivateKey] = key.Bytes()
|
||||
}
|
||||
|
||||
ca, err = cert.NewCertificateAuthorityFromBytes(instance.Data[Cert], instance.Data[PrivateKey])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func filterByName(objName, desired string) bool {
|
||||
return objName == desired
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package secret
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
"github.com/clastix/capsule/pkg/cert"
|
||||
)
|
||||
|
||||
// Add creates a new Secret Controller and adds it to the Manager. The Manager will set fields on the Controller
|
||||
// and Start it when the Manager is Started.
|
||||
func AddCa(mgr manager.Manager) error {
|
||||
r := newReconciler(mgr, "controller_secret", caReconcile)
|
||||
return ca(mgr, r)
|
||||
}
|
||||
|
||||
// add adds a new Controller to mgr with r as the reconcile.Reconciler
|
||||
func ca(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
// Create a new controller
|
||||
c, err := controller.New("secret-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to CA Secret
|
||||
err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForObject{}, predicate.Funcs{
|
||||
CreateFunc: func(event event.CreateEvent) (ok bool) {
|
||||
return filterByName(event.Meta.GetName(), CaSecretName)
|
||||
},
|
||||
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
|
||||
return filterByName(deleteEvent.Meta.GetName(), CaSecretName)
|
||||
},
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
|
||||
return filterByName(updateEvent.MetaNew.GetName(), CaSecretName)
|
||||
},
|
||||
GenericFunc: func(genericEvent event.GenericEvent) bool {
|
||||
return filterByName(genericEvent.Meta.GetName(), CaSecretName)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func caReconcile(r *ReconcileSecret, request reconcile.Request) (reconcile.Result, error) {
|
||||
var err error
|
||||
|
||||
r.logger = r.logger.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
r.logger.Info("Reconciling CA Secret")
|
||||
|
||||
// Fetch the CA instance
|
||||
instance := &corev1.Secret{}
|
||||
err = r.client.Get(context.TODO(), request.NamespacedName, instance)
|
||||
if err != nil {
|
||||
// Error reading the object - requeue the request.
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
var ca cert.Ca
|
||||
var rq time.Duration
|
||||
ca, err = r.GetCertificateAuthority()
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.logger.Info("Handling CA Secret")
|
||||
|
||||
rq, err = ca.ExpiresIn(time.Now())
|
||||
if err != nil {
|
||||
r.logger.Info("CA is expired, cleaning to obtain a new one")
|
||||
instance.Data = map[string][]byte{}
|
||||
} else {
|
||||
r.logger.Info("Updating CA secret with new PEM and RSA")
|
||||
|
||||
var crt *bytes.Buffer
|
||||
var key *bytes.Buffer
|
||||
crt, _ = ca.CaCertificatePem()
|
||||
key, _ = ca.CaPrivateKeyPem()
|
||||
|
||||
instance.Data = map[string][]byte{
|
||||
Cert: crt.Bytes(),
|
||||
PrivateKey: key.Bytes(),
|
||||
}
|
||||
|
||||
wh := &v1.MutatingWebhookConfiguration{}
|
||||
err = r.client.Get(context.TODO(), types.NamespacedName{
|
||||
Name: "capsule",
|
||||
}, wh)
|
||||
if err != nil {
|
||||
r.logger.Error(err, "cannot retrieve MutatingWebhookConfiguration")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
for i, w := range wh.Webhooks {
|
||||
// Updating CABundle only in case of an internal service reference
|
||||
if w.ClientConfig.Service != nil {
|
||||
wh.Webhooks[i].ClientConfig.CABundle = instance.Data[Cert]
|
||||
}
|
||||
}
|
||||
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
return r.client.Update(context.TODO(), wh, &client.UpdateOptions{})
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Error(err, "cannot update MutatingWebhookConfiguration webhooks CA bundle")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta}
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, t, func() error {
|
||||
t.Data = instance.Data
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Error(err, "cannot update Capsule TLS")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if res == controllerutil.OperationResultUpdated {
|
||||
r.logger.Info("Capsule CA has been updated, we need to trigger TLS update too")
|
||||
tls := &corev1.Secret{}
|
||||
err = r.client.Get(context.TODO(), types.NamespacedName{
|
||||
Namespace: "capsuel-system",
|
||||
Name: TlsSecretName,
|
||||
}, tls)
|
||||
if err != nil {
|
||||
r.logger.Error(err, "Capsule TLS Secret missing")
|
||||
}
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
_, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, tls, func() error {
|
||||
tls.Data = map[string][]byte{}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Error(err, "Cannot clean Capsule TLS Secret due to CA update")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.Info("Reconciliation completed, processing back in " + rq.String())
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
"github.com/clastix/capsule/pkg/cert"
|
||||
)
|
||||
|
||||
// Add creates a new Secret Controller and adds it to the Manager. The Manager will set fields on the Controller
|
||||
// and Start it when the Manager is Started.
|
||||
func AddTls(mgr manager.Manager) error {
|
||||
return tls(mgr, newReconciler(mgr, "controller_secret_tls", tlsReconcile))
|
||||
}
|
||||
|
||||
// add adds a new Controller to mgr with r as the reconcile.Reconciler
|
||||
func tls(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
// Create a new controller
|
||||
c, err := controller.New("secret-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to TLS Secret
|
||||
err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForObject{}, predicate.Funcs{
|
||||
CreateFunc: func(event event.CreateEvent) (ok bool) {
|
||||
return filterByName(event.Meta.GetName(), TlsSecretName)
|
||||
},
|
||||
DeleteFunc: func(deleteEvent event.DeleteEvent) bool {
|
||||
return filterByName(deleteEvent.Meta.GetName(), TlsSecretName)
|
||||
},
|
||||
UpdateFunc: func(updateEvent event.UpdateEvent) bool {
|
||||
return filterByName(updateEvent.MetaNew.GetName(), TlsSecretName)
|
||||
},
|
||||
GenericFunc: func(genericEvent event.GenericEvent) bool {
|
||||
return filterByName(genericEvent.Meta.GetName(), TlsSecretName)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tlsReconcile(r *ReconcileSecret, request reconcile.Request) (reconcile.Result, error) {
|
||||
var err error
|
||||
|
||||
r.logger = r.logger.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
|
||||
r.logger.Info("Reconciling TLS/CA Secret")
|
||||
|
||||
// Fetch the Secret instance
|
||||
instance := &corev1.Secret{}
|
||||
err = r.client.Get(context.TODO(), request.NamespacedName, instance)
|
||||
if err != nil {
|
||||
// Error reading the object - requeue the request.
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
var ca cert.Ca
|
||||
var rq time.Duration
|
||||
|
||||
ca, err = r.GetCertificateAuthority()
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
var shouldCreate bool
|
||||
for _, key := range []string{Cert, PrivateKey} {
|
||||
if _, ok := instance.Data[key]; !ok {
|
||||
shouldCreate = true
|
||||
}
|
||||
}
|
||||
|
||||
if shouldCreate {
|
||||
r.logger.Info("Missing Capsule TLS certificate")
|
||||
rq = 6 * 30 * 24 * time.Hour
|
||||
|
||||
opts := cert.NewCertOpts(time.Now().Add(rq), "capsule.capsule-system.svc")
|
||||
crt, key, err := ca.GenerateCertificate(opts)
|
||||
if err != nil {
|
||||
r.logger.Error(err, "Cannot generate new TLS certificate")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
instance.Data = map[string][]byte{
|
||||
Cert: crt.Bytes(),
|
||||
PrivateKey: key.Bytes(),
|
||||
}
|
||||
} else {
|
||||
var c *x509.Certificate
|
||||
var b *pem.Block
|
||||
b, _ = pem.Decode(instance.Data[Cert])
|
||||
c, err = x509.ParseCertificate(b.Bytes)
|
||||
if err != nil {
|
||||
r.logger.Error(err, "cannot parse Capsule TLS")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
rq = time.Duration(c.NotAfter.Unix()-time.Now().Unix()) * time.Second
|
||||
if time.Now().After(c.NotAfter) {
|
||||
r.logger.Info("Capsule TLS is expired, cleaning to obtain a new one")
|
||||
instance.Data = map[string][]byte{}
|
||||
}
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
t := &corev1.Secret{ObjectMeta: instance.ObjectMeta,}
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, t, func() error {
|
||||
t.Data = instance.Data
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Error(err, "cannot update Capsule TLS")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
if instance.Name == TlsSecretName && res == controllerutil.OperationResultUpdated {
|
||||
r.logger.Info("Capsule TLS certificates has been updated, we need to restart the Controller")
|
||||
os.Exit(15)
|
||||
}
|
||||
|
||||
r.logger.Info("Reconciliation completed, processing back in " + rq.String())
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
Copyright 2020 Clastix Labs.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
networkingv1 "k8s.io/api/networking/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
capsulev1alpha1 "github.com/clastix/capsule/pkg/apis/capsule/v1alpha1"
|
||||
)
|
||||
|
||||
// Add creates a new Tenant Controller and adds it to the Manager. The Manager will set fields on the Controller
|
||||
// and Start it when the Manager is Started.
|
||||
func Add(mgr manager.Manager) error {
|
||||
return add(mgr, &ReconcileTenant{
|
||||
client: mgr.GetClient(),
|
||||
scheme: mgr.GetScheme(),
|
||||
logger: log.Log.WithName("controller_tenant"),
|
||||
})
|
||||
}
|
||||
|
||||
// add adds a new Controller to mgr with r as the reconcile.Reconciler
|
||||
func add(mgr manager.Manager, r reconcile.Reconciler) error {
|
||||
// Create a new controller
|
||||
c, err := controller.New("tenant-controller", mgr, controller.Options{Reconciler: r})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for changes to primary resource Tenant
|
||||
err = c.Watch(&source.Kind{Type: &capsulev1alpha1.Tenant{}}, &handler.EnqueueRequestForObject{}, )
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch for controlled resources
|
||||
for _, r := range []runtime.Object{&networkingv1.NetworkPolicy{}, &corev1.LimitRange{}, &corev1.ResourceQuota{}, &rbacv1.RoleBinding{}} {
|
||||
err = c.Watch(&source.Kind{Type: r}, &handler.EnqueueRequestForOwner{
|
||||
IsController: true,
|
||||
OwnerType: &capsulev1alpha1.Tenant{},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReconcileTenant reconciles a Tenant object
|
||||
type ReconcileTenant struct {
|
||||
client client.Client
|
||||
scheme *runtime.Scheme
|
||||
logger logr.Logger
|
||||
}
|
||||
|
||||
// Reconcile reads that state of the cluster for a Tenant object and makes changes based on the state read
|
||||
// and what is in the Tenant.Spec
|
||||
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
|
||||
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
|
||||
func (r *ReconcileTenant) Reconcile(request reconcile.Request) (reconcile.Result, error) {
|
||||
r.logger = log.Log.WithName("controller_tenant").WithValues("Request.Name", request.Name)
|
||||
|
||||
// Fetch the Tenant instance
|
||||
instance := &capsulev1alpha1.Tenant{}
|
||||
err := r.client.Get(context.TODO(), request.NamespacedName, instance)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
r.logger.Info("Request object not found, could have been deleted after reconcile request")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
r.logger.Error(err, "Error reading the object")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.logger.Info("Starting processing of Network Policies", "items", len(instance.Spec.NetworkPolicies))
|
||||
if err := r.syncNetworkPolicies(instance); err != nil {
|
||||
r.logger.Error(err, "Cannot sync NetworkPolicy items")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.logger.Info("Starting processing of Node Selector")
|
||||
if err := r.ensureNodeSelector(instance); err != nil {
|
||||
r.logger.Error(err, "Cannot sync Namespaces Node Selector items")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.logger.Info("Starting processing of Limit Ranges", "items", len(instance.Spec.LimitRanges))
|
||||
if err := r.syncLimitRanges(instance); err != nil {
|
||||
r.logger.Error(err, "Cannot sync LimitRange items")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.logger.Info("Starting processing of Resource Quotas", "items", len(instance.Spec.ResourceQuota))
|
||||
if err := r.syncResourceQuotas(instance); err != nil {
|
||||
r.logger.Error(err, "Cannot sync ResourceQuota items")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.logger.Info("Ensuring RoleBinding for owner")
|
||||
if err := r.ownerRoleBinding(instance); err != nil {
|
||||
r.logger.Error(err, "Cannot sync owner RoleBinding")
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
r.logger.Info("Tenant reconciling completed")
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
|
||||
// NetworkPolicy using the "notin" LabelSelector to perform an outer-join removal.
|
||||
func (r *ReconcileTenant) pruningResources(ns string, keys []string, obj runtime.Object) error {
|
||||
capsuleLabel, err := capsulev1alpha1.GetTypeLabel(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := labels.NewRequirement(capsuleLabel, selection.NotIn, keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.logger.Info("Pruning objects with label selector " + req.String())
|
||||
err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
return r.client.DeleteAllOf(context.TODO(), obj, &client.DeleteAllOfOptions{
|
||||
ListOptions: client.ListOptions{
|
||||
LabelSelector: labels.NewSelector().Add(*req),
|
||||
Namespace: ns,
|
||||
},
|
||||
DeleteOptions: client.DeleteOptions{},
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We're relying on the ResourceQuota resource to represent the resource quota for the single Tenant rather than the
|
||||
// single Namespace, so abusing of this API although its Namespaced scope.
|
||||
// Since a Namespace could take-up all the available resource quota, the Namespace ResourceQuota will be a 1:1 mapping
|
||||
// to the Tenant one: in a second time Capsule is going to sum all the analogous ResourceQuota resources on other Tenant
|
||||
// namespaces to check if the Tenant quota has been exceeded or not, reusing the native Kubernetes policy putting the
|
||||
// .Status.Used value as the .Hard value.
|
||||
// This will trigger a following reconciliation but that's ok: the mutateFn will re-use the same business logic, letting
|
||||
// the mutateFn along with the CreateOrUpdate to don't perform the update since resources are identical.
|
||||
func (r *ReconcileTenant) syncResourceQuotas(tenant *capsulev1alpha1.Tenant) error {
|
||||
// getting requested ResourceQuota keys
|
||||
keys := make([]string, 0, len(tenant.Spec.ResourceQuota))
|
||||
for i := range tenant.Spec.ResourceQuota {
|
||||
keys = append(keys, strconv.Itoa(i))
|
||||
}
|
||||
|
||||
// getting ResourceQuota labels for the mutateFn
|
||||
tenantLabel, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typeLabel, err := capsulev1alpha1.GetTypeLabel(&corev1.ResourceQuota{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ns := range tenant.Status.Namespaces {
|
||||
if err := r.pruningResources(ns, keys, &corev1.ResourceQuota{}); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, q := range tenant.Spec.ResourceQuota {
|
||||
target := &corev1.ResourceQuota{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
|
||||
Namespace: ns,
|
||||
Annotations: make(map[string]string),
|
||||
Labels: map[string]string{
|
||||
tenantLabel: tenant.Name,
|
||||
typeLabel: strconv.Itoa(i),
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.client, target, func() (err error) {
|
||||
// Requirement to list ResourceQuota of the current Tenant
|
||||
tr, err := labels.NewRequirement(tenantLabel, selection.Equals, []string{tenant.Name})
|
||||
if err != nil {
|
||||
r.logger.Error(err, "Cannot build ResourceQuota Tenant requirement")
|
||||
}
|
||||
// Requirement to list ResourceQuota for the current index
|
||||
ir, err := labels.NewRequirement(typeLabel, selection.Equals, []string{strconv.Itoa(i)})
|
||||
if err != nil {
|
||||
r.logger.Error(err, "Cannot build ResourceQuota index requirement")
|
||||
}
|
||||
|
||||
// Listing all the ResourceQuota according to the said requirements.
|
||||
// These are required since Capsule is going to sum all the used quota to
|
||||
// sum them and get the Tenant one.
|
||||
rql := &corev1.ResourceQuotaList{}
|
||||
err = r.client.List(context.TODO(), rql, &client.ListOptions{
|
||||
LabelSelector: labels.NewSelector().Add(*tr).Add(*ir),
|
||||
})
|
||||
if err != nil {
|
||||
r.logger.Error(err, "Cannot list ResourceQuota", "tenantFilter", tr.String(), "indexFilter", ir.String())
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterating over all the options declared for the ResourceQuota,
|
||||
// summing all the used quota across different Namespaces to determinate
|
||||
// if we're hitting a Hard quota at Tenant level.
|
||||
// For this case, we're going to block the Quota setting the Hard as the
|
||||
// used one.
|
||||
for rn, rq := range q.Hard {
|
||||
r.logger.Info("Desired hard " + rn.String() + " quota is " + rq.String())
|
||||
|
||||
// Getting the whole usage across all the Tenant Namespaces
|
||||
var qt resource.Quantity
|
||||
for _, rq := range rql.Items {
|
||||
qt.Add(rq.Status.Used[rn])
|
||||
}
|
||||
r.logger.Info("Computed " + rn.String() + " quota for the whole Tenant is " + qt.String())
|
||||
|
||||
switch qt.Cmp(q.Hard[rn]) {
|
||||
case 1:
|
||||
// The Tenant is OverQuota:
|
||||
// updating all the related ResourceQuota with the current
|
||||
// used Quota to block further creations.
|
||||
for i := range rql.Items {
|
||||
rql.Items[i].Spec.Hard[rn] = rql.Items[i].Status.Used[rn]
|
||||
}
|
||||
println("")
|
||||
default:
|
||||
// The Tenant is respecting the Hard quota:
|
||||
// restoring the default one for all the elements,
|
||||
// also for the reconciliated one.
|
||||
for i := range rql.Items {
|
||||
rql.Items[i].Spec.Hard[rn] = q.Hard[rn]
|
||||
}
|
||||
target.Spec = q
|
||||
}
|
||||
|
||||
// Updating all outer join ResourceQuota adding the Used for the current Resource
|
||||
// TODO(prometherion): this is too expensive, should be performed via a recursion
|
||||
for _, oj := range rql.Items {
|
||||
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
_ = r.client.Get(context.TODO(), types.NamespacedName{Namespace: oj.Namespace, Name: oj.Name}, &oj)
|
||||
if oj.Annotations == nil {
|
||||
oj.Annotations = make(map[string]string)
|
||||
}
|
||||
oj.Annotations[capsulev1alpha1.UsedQuotaFor(rn)] = qt.String()
|
||||
return r.client.Update(context.TODO(), &oj, &client.UpdateOptions{})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return controllerutil.SetControllerReference(tenant, target, r.scheme)
|
||||
})
|
||||
r.logger.Info("Resource Quota sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensuring all the LimitRange are applied to each Namespace handled by the Tenant.
|
||||
func (r *ReconcileTenant) syncLimitRanges(tenant *capsulev1alpha1.Tenant) error {
|
||||
// getting requested LimitRange keys
|
||||
keys := make([]string, 0, len(tenant.Spec.LimitRanges))
|
||||
for i := range tenant.Spec.LimitRanges {
|
||||
keys = append(keys, strconv.Itoa(i))
|
||||
}
|
||||
|
||||
// getting LimitRange labels for the mutateFn
|
||||
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ll, err := capsulev1alpha1.GetTypeLabel(&corev1.LimitRange{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ns := range tenant.Status.Namespaces {
|
||||
if err := r.pruningResources(ns, keys, &corev1.LimitRange{}); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, spec := range tenant.Spec.LimitRanges {
|
||||
t := &corev1.LimitRange{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
|
||||
Namespace: ns,
|
||||
},
|
||||
}
|
||||
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.client, t, func() (err error) {
|
||||
t.ObjectMeta.Labels = map[string]string{
|
||||
tl: tenant.Name,
|
||||
ll: strconv.Itoa(i),
|
||||
}
|
||||
t.Spec = spec
|
||||
return controllerutil.SetControllerReference(tenant, t, r.scheme)
|
||||
})
|
||||
r.logger.Info("LimitRange sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensuring all the NetworkPolicies are applied to each Namespace handled by the Tenant.
|
||||
func (r *ReconcileTenant) syncNetworkPolicies(tenant *capsulev1alpha1.Tenant) error {
|
||||
// getting requested NetworkPolicy keys
|
||||
keys := make([]string, 0, len(tenant.Spec.NetworkPolicies))
|
||||
for i := range tenant.Spec.NetworkPolicies {
|
||||
keys = append(keys, strconv.Itoa(i))
|
||||
}
|
||||
|
||||
// getting NetworkPolicy labels for the mutateFn
|
||||
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nl, err := capsulev1alpha1.GetTypeLabel(&networkingv1.NetworkPolicy{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, ns := range tenant.Status.Namespaces {
|
||||
if err := r.pruningResources(ns, keys, &networkingv1.NetworkPolicy{}); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, spec := range tenant.Spec.NetworkPolicies {
|
||||
t := &networkingv1.NetworkPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("capsule-%s-%d", tenant.Name, i),
|
||||
Namespace: ns,
|
||||
Labels: map[string]string{
|
||||
tl: tenant.Name,
|
||||
nl: strconv.Itoa(i),
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := controllerutil.CreateOrUpdate(context.TODO(), r.client, t, func() (err error) {
|
||||
t.Spec = spec
|
||||
return controllerutil.SetControllerReference(tenant, t, r.scheme)
|
||||
})
|
||||
r.logger.Info("Network Policy sync result: "+string(res), "name", t.Name, "namespace", t.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Each Tenant owner needs the admin Role attached to each Namespace, otherwise no actions on it can be performed.
|
||||
// Since RBAC is based on deny all first, some specific actions like editing Capsule resources are going to be blocked
|
||||
// via Dynamic Admission Webhooks.
|
||||
// TODO(prometherion): we could create a capsule:admin role rather than hitting webhooks for each action
|
||||
func (r *ReconcileTenant) ownerRoleBinding(tenant *capsulev1alpha1.Tenant) error {
|
||||
// getting RoleBinding label for the mutateFn
|
||||
tl, err := capsulev1alpha1.GetTypeLabel(&capsulev1alpha1.Tenant{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := map[string]string{tl: tenant.Name}
|
||||
s := []rbacv1.Subject{
|
||||
{
|
||||
Kind: "User",
|
||||
Name: tenant.Spec.Owner,
|
||||
},
|
||||
}
|
||||
|
||||
rbl := make(map[types.NamespacedName]rbacv1.RoleRef)
|
||||
for _, i := range tenant.Status.Namespaces {
|
||||
rbl[types.NamespacedName{Namespace: i, Name: "namespace:admin"}] = rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "admin",
|
||||
}
|
||||
rbl[types.NamespacedName{Namespace: i, Name: "namespace:deleter"}] = rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "namespace:deleter",
|
||||
}
|
||||
}
|
||||
|
||||
for nn, rr := range rbl {
|
||||
target := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: nn.Name,
|
||||
Namespace: nn.Namespace,
|
||||
},
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, target, func() (err error) {
|
||||
target.ObjectMeta.Labels = l
|
||||
target.Subjects = s
|
||||
target.RoleRef = rr
|
||||
return controllerutil.SetControllerReference(tenant, target, r.scheme)
|
||||
})
|
||||
r.logger.Info("Role Binding sync result: "+string(res), "name", target.Name, "namespace", target.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReconcileTenant) ensureNodeSelector(tenant *capsulev1alpha1.Tenant) (err error) {
|
||||
if tenant.Spec.NodeSelector == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, namespace := range tenant.Status.Namespaces {
|
||||
selectorMap := tenant.Spec.NodeSelector
|
||||
if selectorMap == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
|
||||
var res controllerutil.OperationResult
|
||||
res, err = controllerutil.CreateOrUpdate(context.TODO(), r.client, ns, func() error {
|
||||
if ns.Annotations == nil {
|
||||
ns.Annotations = make(map[string]string)
|
||||
}
|
||||
var selector []string
|
||||
for k, v := range selectorMap {
|
||||
selector = append(selector, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
ns.Annotations["scheduler.alpha.kubernetes.io/node-selector"] = strings.Join(selector, ",")
|
||||
return nil
|
||||
})
|
||||
r.logger.Info("Namespace Node sync result: "+string(res), "name", ns.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user