ceph: move the admission webhook to the operator

Our admission webhooks will now run as part of the Operator container
and not an additional deployment. This has the advantage of consuming
fewer resources in the cluster and not having to manage affinities and
tolerations. This only drawback is that the Secret containing the
certificates is not mounted anymore and the content needs to be written
inside the Operator. This is not practical since we also need to watch
for the Secret content to change. Meaning that the certificates have
been renewed and the webhook server needs to use them.
A new approach is on its way to hopefully simplify this last issue and
implement a watcher for the Secret.
In the meantime, users need to use the cert-manager or renew
certificates manually. Additionally, they must update the
ValidatingWebhookConfiguration object with the new CA bundle.

Signed-off-by: Sébastien Han <seb@redhat.com>
This commit is contained in:
Sébastien Han committed 2021-08-24 19:07:04 +02:00
1 parent 6582bac8e7
commit 656dd0f334
17 files changed
+133 -575

No files matched your search

@@ -471,12 +471,3 @@ rules:
verbs:
- use
{{- end }}
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-ceph-admission-controller-role
rules:
- apiGroups: ["ceph.rook.io"]
resources: ["*"]
verbs: ["get", "watch", "list"]
@@ -260,20 +260,6 @@ subjects:
name: rook-ceph-mgr
namespace: {{ .Release.Namespace }}
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-ceph-admission-controller-rolebinding
subjects:
- kind: ServiceAccount
name: rook-ceph-admission-controller
apiGroup: ""
namespace: {{ .Release.Namespace }}
roleRef:
kind: ClusterRole
name: rook-ceph-admission-controller-role
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
@@ -313,20 +313,6 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
{{- if .Values.admissionController }}
{{- if .Values.admissionController.tolerations }}
- name: ADMISSION_CONTROLLER_TOLERATIONS
value: {{ toYaml .Values.admissionController.tolerations | quote }}
{{- end }}
{{- if .Values.admissionController.nodeAffinity }}
- name: ADMISSION_CONTROLLER_NODE_AFFINITY
value: {{ .Values.admissionController.nodeAffinity }}
{{- end }}
{{- end }}
{{- if .Values.unreachableNodeTolerationSeconds }}
- name: ROOK_UNREACHABLE_NODE_TOLERATION_SECONDS
value: {{ .Values.unreachableNodeTolerationSeconds | quote }}
{{- end }}
{{- if .Values.resources }}
resources:
{{ toYaml .Values.resources | indent 10 }}
@@ -77,12 +77,6 @@ metadata:
namespace: {{ .Release.Namespace }}
{{ template "imagePullSecrets" . }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: rook-ceph-admission-controller
namespace: {{ .Release.Namespace }}
---
# Service account for the purge osd job
apiVersion: v1
kind: ServiceAccount
@@ -90,4 +84,4 @@ metadata:
name: rook-ceph-purge-osd
namespace: {{ .Release.Namespace }}
{{ template "imagePullSecrets" . }}
---
---
@@ -32,35 +32,6 @@ subjects:
# OLM: END OBJECTBUCKET ROLEBINDING
# OLM: BEGIN OPERATOR ROLE
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: rook-ceph-admission-controller
namespace: rook-ceph # namespace:operator
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-ceph-admission-controller-role
rules:
- apiGroups: ["ceph.rook.io"]
resources: ["*"]
verbs: ["get", "watch", "list"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rook-ceph-admission-controller-rolebinding
subjects:
- kind: ServiceAccount
name: rook-ceph-admission-controller
apiGroup: ""
namespace: rook-ceph # namespace:operator
roleRef:
kind: ClusterRole
name: rook-ceph-admission-controller-role
apiGroup: rbac.authorization.k8s.io
---
# The cluster role for managing all the cluster-specific resources in a namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
@@ -405,18 +405,6 @@ data:
# The timeout value (in seconds) of Ceph commands. It should be >= 1. If this variable is not set or is an invalid value, it's default to 15.
ROOK_CEPH_COMMANDS_TIMEOUT_SECONDS: "15"
# CSI_VOLUME_REPLICATION_IMAGE: "quay.io/csiaddons/volumereplication-operator:v0.1.0"
# (Optional) Admission controller NodeAffinity.
# ADMISSION_CONTROLLER_NODE_AFFINITY: "role=storage-node; storage=rook, ceph"
# (Optional) Admission controller tolerations list. Put here list of taints you want to tolerate in YAML format.
# Admission controller would be best to start on the same nodes as other ceph daemons.
# ADMISSION_CONTROLLER_TOLERATIONS: |
# - effect: NoSchedule
# key: node-role.kubernetes.io/controlplane
# operator: Exists
# - effect: NoExecute
# key: node-role.kubernetes.io/etcd
# operator: Exists
---
# The deployment for the rook operator
# OLM: BEGIN OPERATOR DEPLOYMENT
@@ -448,6 +436,12 @@ spec:
name: rook-config
- mountPath: /etc/ceph
name: default-config-dir
- mountPath: /etc/webhook
name: webhook-cert
ports:
- containerPort: 9443
name: https-webhook
protocol: TCP
env:
- name: ROOK_CURRENT_NAMESPACE_ONLY
value: "false"
@@ -577,4 +571,6 @@ spec:
emptyDir: {}
- name: default-config-dir
emptyDir: {}
- name: webhook-cert
emptyDir: {}
# OLM: END OPERATOR DEPLOYMENT
+8 -12
View File
@@ -329,18 +329,6 @@ data:
# Enable volume replication controller
CSI_ENABLE_VOLUME_REPLICATION: "false"
# CSI_VOLUME_REPLICATION_IMAGE: "quay.io/csiaddons/volumereplication-operator:v0.1.0"
# (Optional) Admission controller NodeAffinity.
# ADMISSION_CONTROLLER_NODE_AFFINITY: "role=storage-node; storage=rook, ceph"
# (Optional) Admission controller tolerations list. Put here list of taints you want to tolerate in YAML format.
# Admission controller would be best to start on the same nodes as other ceph daemons.
# ADMISSION_CONTROLLER_TOLERATIONS: |
# - effect: NoSchedule
# key: node-role.kubernetes.io/controlplane
# operator: Exists
# - effect: NoExecute
# key: node-role.kubernetes.io/etcd
# operator: Exists
---
# OLM: BEGIN OPERATOR DEPLOYMENT
apiVersion: apps/v1
@@ -371,6 +359,12 @@ spec:
name: rook-config
- mountPath: /etc/ceph
name: default-config-dir
- mountPath: /etc/webhook
name: webhook-cert
ports:
- containerPort: 9443
name: https-webhook
protocol: TCP
env:
# If the operator should only watch for cluster CRDs in the same namespace, set this to "true".
# If this is not set to true, the operator will watch for cluster CRDs in all namespaces.
@@ -522,4 +516,6 @@ spec:
emptyDir: {}
- name: default-config-dir
emptyDir: {}
- name: webhook-cert
emptyDir: {}
# OLM: END OPERATOR DEPLOYMENT
-43
View File
@@ -1,43 +0,0 @@
/*
Copyright 2020 The Rook Authors. All rights reserved.
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 ceph
import (
"github.com/rook/rook/cmd/rook/rook"
operator "github.com/rook/rook/pkg/operator/ceph"
"github.com/spf13/cobra"
)
var (
admissionCmd = &cobra.Command{
Use: "admission-controller",
Short: "Starts admission controller",
}
)
func init() {
admissionCmd.Run = startAdmissionController
}
func startAdmissionController(cmd *cobra.Command, args []string) {
rook.SetLogLevel()
rook.LogStartupInfo(admissionCmd.Flags())
err := operator.StartAdmissionController()
if err != nil {
rook.TerminateFatal(err)
}
}
-1
View File
@@ -58,7 +58,6 @@ func init() {
Cmd.AddCommand(cleanUpCmd,
operatorCmd,
agentCmd,
admissionCmd,
osdCmd,
mgrCmd,
configCmd)
+1 -1
View File
@@ -84,7 +84,7 @@ func startOperator(cmd *cobra.Command, args []string) error {
op := operator.New(context, volumeAttachment, rookImage, serviceAccountName)
err = op.Run()
if err != nil {
rook.TerminateFatal(errors.Wrap(err, "failed to run operator\n"))
rook.TerminateFatal(errors.Wrap(err, "failed to run operator"))
}
return nil
@@ -27,6 +27,8 @@ import (
)
// StartOperatorSettingsWatch starts the operator settings watcher
// TODO: use controller runtime, so we can use a Context to cancel the watch instead of using another channel
// The cache package mentions that it'd be nice to use a Context for cancellation too
func (c *ClusterController) StartOperatorSettingsWatch(stopCh chan struct{}) {
operatorNamespace := os.Getenv(k8sutil.PodNamespaceEnvVar)
// watch for "rook-ceph-operator-config" ConfigMap
+40 -12
View File
@@ -20,7 +20,6 @@ import (
"context"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/apis/rook.io/v1alpha2"
"github.com/rook/rook/pkg/operator/ceph/cluster"
"github.com/rook/rook/pkg/operator/ceph/disruption/controllerconfig"
@@ -28,9 +27,15 @@ import (
mapiv1 "github.com/openshift/cluster-api/pkg/apis/machine/v1beta1"
healthchecking "github.com/openshift/machine-api-operator/pkg/apis/healthchecking/v1alpha1"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/config"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
const (
certDir = "/etc/webhook"
)
var (
@@ -43,7 +48,11 @@ var (
}
)
func (o *Operator) startManager(namespaceToWatch string, context context.Context, mgrErrorCh chan error) {
var (
webhookResources = []webhook.Validator{&cephv1.CephCluster{}, &cephv1.CephBlockPool{}, &cephv1.CephObjectStore{}}
)
func (o *Operator) startManager(context context.Context, namespaceToWatch string, mgrErrorCh chan error) {
logger.Info("setting up schemes")
// Setup Scheme for all resources
scheme := runtime.NewScheme()
@@ -60,16 +69,11 @@ func (o *Operator) startManager(namespaceToWatch string, context context.Context
LeaderElection: false,
Namespace: namespaceToWatch,
Scheme: scheme,
CertDir: certDir,
}
logger.Info("setting up the controller-runtime manager")
kubeConfig, err := config.GetConfig()
if err != nil {
mgrErrorCh <- errors.Wrap(err, "failed to get client config for controller-runtime manager")
return
}
mgr, err := manager.New(kubeConfig, mgrOpts)
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOpts)
if err != nil {
mgrErrorCh <- errors.Wrap(err, "failed to set up overall controller-runtime manager")
return
@@ -77,11 +81,12 @@ func (o *Operator) startManager(namespaceToWatch string, context context.Context
// options to pass to the controllers
controllerOpts := &controllerconfig.Context{
RookImage: o.rookImage,
RookImage: o.config.Image,
ClusterdContext: o.context,
OperatorNamespace: o.operatorNamespace,
OperatorNamespace: o.config.OperatorNamespace,
ReconcileCanaries: &controllerconfig.LockingBool{},
}
// Add the registered controllers to the manager (entrypoint for controllers)
err = cluster.AddToManager(mgr, controllerOpts, o.clusterController)
if err != nil {
@@ -89,6 +94,29 @@ func (o *Operator) startManager(namespaceToWatch string, context context.Context
return
}
// Add Webhooks
// TODO: this needs a callback with a watcher for this secret so we can quickly enable the webhook if it changes
isPresent, err := isSecretPresent(context, o.context)
if err != nil {
mgrErrorCh <- errors.Wrap(err, "failed to retrieve admission webhook secret")
return
}
if isPresent {
err := createWebhookService(o.context)
if err != nil {
mgrErrorCh <- errors.Wrap(err, "failed to create admission webhook service")
return
}
logger.Info("setting up admission webhooks")
for _, resource := range webhookResources {
err = ctrl.NewWebhookManagedBy(mgr).For(resource).Complete()
if err != nil {
mgrErrorCh <- errors.Wrapf(err, "failed to register webhook for %q", resource.GetObjectKind().GroupVersionKind().Kind)
return
}
}
}
logger.Info("starting the controller-runtime manager")
if err := mgr.Start(context); err != nil {
mgrErrorCh <- errors.Wrap(err, "failed to run the controller-runtime manager")
+30 -29
View File
@@ -68,28 +68,33 @@ var (
// Operator type for managing storage
type Operator struct {
context *clusterd.Context
resources []k8sutil.CustomResource
operatorNamespace string
rookImage string
securityAccount string
context *clusterd.Context
resources []k8sutil.CustomResource
config *Config
// The custom resource that is global to the kubernetes cluster.
// The cluster is global because you create multiple clusters in k8s
clusterController *cluster.ClusterController
delayedDaemonsStarted bool
}
type Config struct {
OperatorNamespace string
Image string
ServiceAccount string
}
// New creates an operator instance
func New(context *clusterd.Context, volumeAttachmentWrapper attachment.Attachment, rookImage, securityAccount string) *Operator {
func New(context *clusterd.Context, volumeAttachmentWrapper attachment.Attachment, rookImage, serviceAccount string) *Operator {
schemes := []k8sutil.CustomResource{opcontroller.ClusterResource, attachment.VolumeResource}
operatorNamespace := os.Getenv(k8sutil.PodNamespaceEnvVar)
o := &Operator{
context: context,
resources: schemes,
operatorNamespace: operatorNamespace,
rookImage: rookImage,
securityAccount: securityAccount,
context: context,
resources: schemes,
config: &Config{
OperatorNamespace: os.Getenv(k8sutil.PodNamespaceEnvVar),
Image: rookImage,
ServiceAccount: serviceAccount,
},
}
operatorConfigCallbacks := []func() error{
o.updateDrivers,
@@ -126,10 +131,11 @@ func (o *Operator) updateOperatorLogLevel() error {
// Run the operator instance
func (o *Operator) Run() error {
if o.operatorNamespace == "" {
if o.config.OperatorNamespace == "" {
return errors.Errorf("rook operator namespace is not provided. expose it via downward API in the rook operator manifest file using environment variable %q", k8sutil.PodNamespaceEnvVar)
}
// TODO: needs a callback?
opcontroller.SetCephCommandsTimeout(o.context)
// Initialize signal handler and context
@@ -138,20 +144,15 @@ func (o *Operator) Run() error {
rookDiscover := discover.New(o.context.Clientset)
if opcontroller.DiscoveryDaemonEnabled(o.context) {
if err := rookDiscover.Start(o.operatorNamespace, o.rookImage, o.securityAccount, true); err != nil {
if err := rookDiscover.Start(o.config.OperatorNamespace, o.config.Image, o.config.ServiceAccount, true); err != nil {
return errors.Wrap(err, "failed to start device discovery daemonset")
}
} else {
if err := rookDiscover.Stop(stopContext, o.operatorNamespace); err != nil {
if err := rookDiscover.Stop(stopContext, o.config.OperatorNamespace); err != nil {
return errors.Wrap(err, "failed to stop device discovery daemonset")
}
}
logger.Debug("checking for admission controller secrets")
err := StartControllerIfSecretPresent(stopContext, o.context, o.rookImage)
if err != nil {
return errors.Wrap(err, "failed to start webhook")
}
serverVersion, err := o.context.Clientset.Discovery().ServerVersion()
if err != nil {
return errors.Wrap(err, "failed to get server version")
@@ -178,7 +179,7 @@ func (o *Operator) Run() error {
var namespaceToWatch string
if os.Getenv("ROOK_CURRENT_NAMESPACE_ONLY") == "true" {
logger.Infof("watching the current namespace for a ceph cluster CR")
namespaceToWatch = o.operatorNamespace
namespaceToWatch = o.config.OperatorNamespace
} else {
logger.Infof("watching all namespaces for ceph cluster CRs")
namespaceToWatch = v1.NamespaceAll
@@ -186,7 +187,7 @@ func (o *Operator) Run() error {
// Start the controller-runtime Manager.
mgrErrorChan := make(chan error)
go o.startManager(namespaceToWatch, stopContext, mgrErrorChan)
go o.startManager(stopContext, namespaceToWatch, mgrErrorChan)
// Start the operator setting watcher
go o.clusterController.StartOperatorSettingsWatch(stopChan)
@@ -198,8 +199,8 @@ func (o *Operator) Run() error {
logger.Infof("shutdown signal received, exiting... %v", stopContext.Err())
o.cleanup(stopChan)
return nil
case err := <-mgrErrorChan:
logger.Errorf("gave up to run the operator. %v", err)
o.cleanup(stopChan)
return err
}
@@ -228,13 +229,13 @@ func (o *Operator) updateDrivers() error {
return nil
}
if o.operatorNamespace == "" {
if o.config.OperatorNamespace == "" {
return errors.Errorf("rook operator namespace is not provided. expose it via downward API in the rook operator manifest file using environment variable %s", k8sutil.PodNamespaceEnvVar)
}
if opcontroller.FlexDriverEnabled(o.context) {
rookAgent := agent.New(o.context.Clientset)
if err := rookAgent.Start(o.operatorNamespace, o.rookImage, o.securityAccount); err != nil {
if err := rookAgent.Start(o.config.OperatorNamespace, o.config.Image, o.config.ServiceAccount); err != nil {
return errors.Wrap(err, "error starting agent daemonset")
}
}
@@ -252,7 +253,7 @@ func (o *Operator) updateDrivers() error {
return nil
}
ownerRef, err := getDeploymentOwnerReference(o.context.Clientset, o.operatorNamespace)
ownerRef, err := getDeploymentOwnerReference(o.context.Clientset, o.config.OperatorNamespace)
if err != nil {
logger.Warningf("could not find deployment owner reference to assign to csi drivers. %v", err)
}
@@ -261,15 +262,15 @@ func (o *Operator) updateDrivers() error {
ownerRef.BlockOwnerDeletion = &blockOwnerDeletion
}
ownerInfo := k8sutil.NewOwnerInfoWithOwnerRef(ownerRef, o.operatorNamespace)
ownerInfo := k8sutil.NewOwnerInfoWithOwnerRef(ownerRef, o.config.OperatorNamespace)
// create an empty config map. config map will be filled with data
// later when clusters have mons
err = csi.CreateCsiConfigMap(o.operatorNamespace, o.context.Clientset, ownerInfo)
err = csi.CreateCsiConfigMap(o.config.OperatorNamespace, o.context.Clientset, ownerInfo)
if err != nil {
return errors.Wrap(err, "failed creating csi config map")
}
go csi.ValidateAndConfigureDrivers(o.context, o.operatorNamespace, o.rookImage, o.securityAccount, serverVersion, ownerInfo)
go csi.ValidateAndConfigureDrivers(o.context, o.config.OperatorNamespace, o.config.Image, o.config.ServiceAccount, serverVersion, ownerInfo)
return nil
}
-68
View File
@@ -1,68 +0,0 @@
/*
Copyright 2020 The Rook Authors. All rights reserved.
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 operator
import (
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
var (
scheme = runtime.NewScheme()
resources = []webhook.Validator{&cephv1.CephCluster{}, &cephv1.CephBlockPool{}, &cephv1.CephObjectStore{}}
)
const (
// Default directory where TLS certs are stored
certDir = "/etc/webhook"
// Default port for server
port = 8079
)
// StartAdmissionController will start the server
func StartAdmissionController() error {
logger.Infof("starting the webhook for backend ceph")
err := cephv1.AddToScheme(scheme)
if err != nil {
return errors.Wrap(err, "failed to add to scheme")
}
opts := ctrl.Options{
Scheme: scheme,
Port: port,
CertDir: certDir,
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), opts)
if err != nil {
return errors.Wrap(err, "failed to create manager")
}
for _, resource := range resources {
err = ctrl.NewWebhookManagedBy(mgr).For(resource).Complete()
if err != nil {
return errors.Wrap(err, "failed to register webhooks")
}
}
logger.Info("starting webhook server")
err = mgr.Start(ctrl.SetupSignalHandler())
if err != nil {
return errors.Wrap(err, "failed to start server")
}
return nil
}
+43 -182
View File
@@ -18,31 +18,23 @@ package operator
import (
"context"
"fmt"
"io/ioutil"
"os"
"github.com/pkg/errors"
"github.com/rook/rook/pkg/clusterd"
"github.com/rook/rook/pkg/operator/ceph/controller"
"github.com/rook/rook/pkg/operator/ceph/csi"
"github.com/rook/rook/pkg/operator/k8sutil"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
const (
appName = "rook-ceph-admission-controller"
secretVolumeName = "webhook-certificates" // #nosec G101 This is just a var name, not a real secret
serviceAccountName = "rook-ceph-admission-controller"
portName = "webhook-api"
servicePort int32 = 443
serverPort int32 = 8079
tlsDir = "/etc/webhook"
admissionControllerTolerationsEnv = "ADMISSION_CONTROLLER_TOLERATIONS"
admissionControllerNodeAffinityEnv = "ADMISSION_CONTROLLER_NODE_AFFINITY"
appName = "rook-ceph-admission-controller"
tlsPort int32 = 443
)
var (
@@ -50,16 +42,30 @@ var (
)
func isSecretPresent(ctx context.Context, context *clusterd.Context) (bool, error) {
logger.Infof("looking for secret %q", appName)
_, err := context.Clientset.CoreV1().Secrets(namespace).Get(ctx, appName, metav1.GetOptions{})
logger.Infof("looking for admission webhook secret %q", appName)
s, err := context.Clientset.CoreV1().Secrets(namespace).Get(ctx, appName, metav1.GetOptions{})
if err != nil {
// If secret is not found. All good ! Proceed with rook without admission controllers
if apierrors.IsNotFound(err) {
logger.Infof("secret %q not found. proceeding without the admission controller", appName)
logger.Infof("admission webhook secret %q not found. proceeding without the admission controller", appName)
return false, nil
}
return false, err
}
// Search for any previous admission controller deployment and if so removing it
logger.Debug("searching for old admission controller deployment")
removeOldAdmissionControllerDeployment(ctx, context)
logger.Infof("admission webhook secret %q found", appName)
for k, data := range s.Data {
path := fmt.Sprintf("%s/%s", certDir, k)
err := ioutil.WriteFile(path, data, 0400)
if err != nil {
return false, errors.Wrapf(err, "failed to write secret content to file %q", path)
}
}
return true, nil
}
@@ -72,189 +78,44 @@ func createWebhookService(context *clusterd.Context) error {
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Port: servicePort,
Port: tlsPort,
TargetPort: intstr.IntOrString{
IntVal: serverPort,
IntVal: int32(webhook.DefaultPort),
},
},
},
Selector: map[string]string{
k8sutil.AppAttr: appName,
k8sutil.AppAttr: "rook-ceph-operator",
},
},
}
_, err := k8sutil.CreateOrUpdateService(context.Clientset, namespace, &webhookService)
if err != nil && !apierrors.IsAlreadyExists(err) {
if err != nil {
return err
}
return nil
}
// StartControllerIfSecretPresent will initialize the webhook if secret is detected
func StartControllerIfSecretPresent(ctx context.Context, context *clusterd.Context, admissionImage string) error {
isPresent, err := isSecretPresent(ctx, context)
func removeOldAdmissionControllerDeployment(ctx context.Context, context *clusterd.Context) {
opts := metav1.ListOptions{LabelSelector: fmt.Sprintf("app=%s", appName)}
d, err := context.Clientset.AppsV1().Deployments(namespace).List(ctx, opts)
if err != nil {
return errors.Wrap(err, "failed to retrieve secret")
logger.Warningf("failed to get old admission controller deployment. %v", err)
return
}
if isPresent {
err = initWebhook(ctx, context, admissionImage)
if err != nil {
return errors.Wrap(err, "failed to initialize webhook")
if len(d.Items) > 0 {
for _, deploy := range d.Items {
var gracePeriod int64
propagation := metav1.DeletePropagationForeground
deleteOpts := metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod, PropagationPolicy: &propagation}
if err = context.Clientset.AppsV1().Deployments(namespace).Delete(ctx, deploy.Name, deleteOpts); err != nil {
logger.Warningf("failed to delete admission controller deployment %q. %v", deploy.Name, err)
return
}
}
logger.Info("successfully removed old admission controller deployment. please remove old service account, cluster role and bindings manually")
}
return nil
}
func initWebhook(ctx context.Context, context *clusterd.Context, admissionImage string) error {
// At this point volume should be mounted, so proceed with creating the service and validatingwebhookconfig
err := createWebhookService(context)
if err != nil {
return errors.Wrap(err, "failed to create service")
}
err = createWebhookDeployment(ctx, context, admissionImage)
if err != nil {
return errors.Wrap(err, "failed to create deployment")
}
return nil
}
func createWebhookDeployment(ctx context.Context, context *clusterd.Context, admissionImage string) error {
logger.Info("creating admission controller pods")
admission_parameters := []string{"ceph",
"admission-controller"}
secretVolume := getSecretVolume()
secretVolumeMount := getSecretVolumeMount()
antiAffinity := csi.GetPodAntiAffinity(k8sutil.AppAttr, appName)
admissionControllerDeployment := getDeployment(ctx, context, secretVolume, antiAffinity, admissionImage, admission_parameters, secretVolumeMount)
_, err := k8sutil.CreateOrUpdateDeployment(context.Clientset, &admissionControllerDeployment)
if err != nil {
return errors.Wrap(err, "failed to create admission-controller deployment")
}
return nil
}
func getDeployment(ctx context.Context, context *clusterd.Context, secretVolume corev1.Volume, antiAffinity corev1.PodAntiAffinity,
admissionImage string, admission_parameters []string, secretVolumeMount corev1.VolumeMount) v1.Deployment {
var replicas int32 = 2
nodes, err := context.Clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
if err == nil {
if len(nodes.Items) == 1 {
replicas = 1
}
} else {
logger.Errorf("failed to get nodes. Defaulting the number of replicas of admission controller pods to 2. %v", err)
}
admissionControllerDeployment := v1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: namespace,
},
Spec: v1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
k8sutil.AppAttr: appName,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: namespace,
Labels: map[string]string{
k8sutil.AppAttr: appName,
},
},
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
secretVolume,
},
Containers: []corev1.Container{
{
Name: appName,
Image: admissionImage,
Args: admission_parameters,
Ports: []corev1.ContainerPort{
{
Name: portName,
ContainerPort: serverPort,
},
},
VolumeMounts: []corev1.VolumeMount{
secretVolumeMount,
},
},
},
ServiceAccountName: serviceAccountName,
Affinity: &corev1.Affinity{
PodAntiAffinity: &antiAffinity,
NodeAffinity: getNodeAffinity(context.Clientset),
},
Tolerations: getTolerations(context.Clientset),
},
},
},
}
return admissionControllerDeployment
}
func getSecretVolumeMount() corev1.VolumeMount {
secretVolumeMount := corev1.VolumeMount{
Name: secretVolumeName,
ReadOnly: true,
MountPath: tlsDir,
}
return secretVolumeMount
}
func getSecretVolume() corev1.Volume {
secretVolume := corev1.Volume{
Name: secretVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: appName,
},
},
}
return secretVolume
}
func getTolerations(clientset kubernetes.Interface) []corev1.Toleration {
// Add toleration if any
tolerations := []corev1.Toleration{}
tolerationsRaw, err := k8sutil.GetOperatorSetting(clientset, controller.OperatorSettingConfigMapName, admissionControllerTolerationsEnv, "")
if err != nil {
logger.Warningf("toleration will be empty because failed to read the setting. %v", err)
return tolerations
}
tolerations, err = k8sutil.YamlToTolerations(tolerationsRaw)
if err != nil {
logger.Warningf("toleration will be empty because failed to parse the setting %q. %v", tolerationsRaw, err)
return tolerations
}
return tolerations
}
func getNodeAffinity(clientset kubernetes.Interface) *corev1.NodeAffinity {
// Add NodeAffinity if any
v1NodeAffinity := &corev1.NodeAffinity{}
nodeAffinity, err := k8sutil.GetOperatorSetting(clientset, controller.OperatorSettingConfigMapName, admissionControllerNodeAffinityEnv, "")
if err != nil {
// nodeAffinity will be empty by default in case of error
logger.Warningf("node affinity will be empty because failed to read the setting. %v", err)
return v1NodeAffinity
}
if nodeAffinity != "" {
v1NodeAffinity, err = k8sutil.GenerateNodeAffinity(nodeAffinity)
if err != nil {
logger.Warningf("node affinity will be empty because failed to parse the setting %q. %v", nodeAffinity, err)
return v1NodeAffinity
}
}
return v1NodeAffinity
}
-134
View File
@@ -1,134 +0,0 @@
/*
Copyright 2020 The Rook Authors. All rights reserved.
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 operator
import (
"context"
"os"
"testing"
"github.com/rook/rook/pkg/operator/ceph/controller"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
const (
testNamespace = "test-namespace"
)
func TestGetTolerations(t *testing.T) {
ctx := context.TODO()
clientset := fake.NewSimpleClientset()
os.Setenv("POD_NAMESPACE", testNamespace)
// No setting results in the default value.
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: controller.OperatorSettingConfigMapName,
Namespace: testNamespace,
},
Data: map[string]string{},
}
_, err := clientset.CoreV1().ConfigMaps(testNamespace).Create(ctx, cm, metav1.CreateOptions{})
assert.NoError(t, err)
tolerations := getTolerations(clientset)
expected := []v1.Toleration{}
assert.Equal(t, expected, tolerations)
// The invalid setting results in the default value.
cm.Data = map[string]string{
admissionControllerTolerationsEnv: "",
}
_, err = clientset.CoreV1().ConfigMaps(testNamespace).Update(ctx, cm, metav1.UpdateOptions{})
assert.NoError(t, err)
tolerations = getTolerations(clientset)
assert.Equal(t, expected, tolerations)
// Correct setting result in the desired value.
cm.Data = map[string]string{
admissionControllerTolerationsEnv: `
- effect: NoSchedule
key: node-role.kubernetes.io/controlplane
operator: Exists`,
}
_, err = clientset.CoreV1().ConfigMaps(testNamespace).Update(ctx, cm, metav1.UpdateOptions{})
assert.NoError(t, err)
tolerations = getTolerations(clientset)
expected = []v1.Toleration{
{
Effect: "NoSchedule",
Key: "node-role.kubernetes.io/controlplane",
Operator: v1.TolerationOpExists,
},
}
assert.Equal(t, expected, tolerations)
}
func TestGetNodeAffinity(t *testing.T) {
ctx := context.TODO()
clientset := fake.NewSimpleClientset()
os.Setenv("POD_NAMESPACE", testNamespace)
// No setting results in the default value.
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: controller.OperatorSettingConfigMapName,
Namespace: testNamespace,
},
Data: map[string]string{},
}
_, err := clientset.CoreV1().ConfigMaps(testNamespace).Create(ctx, cm, metav1.CreateOptions{})
assert.NoError(t, err)
nodeAffinity := getNodeAffinity(clientset)
expected := &v1.NodeAffinity{}
assert.Equal(t, expected, nodeAffinity)
// The invalid setting results in the default value.
cm.Data = map[string]string{
admissionControllerNodeAffinityEnv: "",
}
_, err = clientset.CoreV1().ConfigMaps(testNamespace).Update(ctx, cm, metav1.UpdateOptions{})
assert.NoError(t, err)
nodeAffinity = getNodeAffinity(clientset)
assert.Equal(t, expected, nodeAffinity)
// Correct setting result in the desired value.
cm.Data = map[string]string{
admissionControllerNodeAffinityEnv: "role=storage-node",
}
_, err = clientset.CoreV1().ConfigMaps(testNamespace).Update(ctx, cm, metav1.UpdateOptions{})
assert.NoError(t, err)
nodeAffinity = getNodeAffinity(clientset)
expected = &v1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: "role",
Operator: v1.NodeSelectorOpIn,
Values: []string{"storage-node"},
},
},
},
},
},
}
assert.Equal(t, expected, nodeAffinity)
}
@@ -466,14 +466,6 @@ func (h *CephInstaller) installRookOperator() (bool, error) {
return false, err
}
// disable admission controller test for Kubernetes version older than v1.16.0
if h.settings.EnableAdmissionController && !utils.IsPlatformOpenShift() && h.k8shelper.VersionAtLeast("v1.16.0") {
if !h.k8shelper.IsPodInExpectedState("rook-ceph-admission-controller", h.settings.OperatorNamespace, "Running") {
assert.Fail(h.T(), "admission controller is not running")
return false, errors.Errorf("admission controller is not running")
}
}
discovery, err := h.k8shelper.Clientset.AppsV1().DaemonSets(h.settings.OperatorNamespace).Get(ctx, "rook-discover", metav1.GetOptions{})
if startDiscovery {
assert.NoError(h.T(), err)