mirror of
https://github.com/kubevirt/containerized-data-importer.git
synced 2025-06-03 06:30:22 +00:00

* make deps-update Signed-off-by: Michael Henriksen <mhenriks@redhat.com> * ReourceRequirements -> VolumeResourceRequirements Signed-off-by: Michael Henriksen <mhenriks@redhat.com> * fix calls to controller.Watch() controller-runtime changed the API! Signed-off-by: Michael Henriksen <mhenriks@redhat.com> * Fix errors with actual openshift/library-go lib Signed-off-by: Michael Henriksen <mhenriks@redhat.com> * make all works now and everything compiles Signed-off-by: Michael Henriksen <mhenriks@redhat.com> * fix "make update-codegen" because generate_groups.sh deprecated Signed-off-by: Michael Henriksen <mhenriks@redhat.com> * run "make generate" Signed-off-by: Michael Henriksen <mhenriks@redhat.com> * fix transfer unittest because of change to controller-runtime Signed-off-by: Michael Henriksen <mhenriks@redhat.com> --------- Signed-off-by: Michael Henriksen <mhenriks@redhat.com>
909 lines
31 KiB
Go
909 lines
31 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-logr/logr"
|
|
ocpconfigv1 "github.com/openshift/api/config/v1"
|
|
routev1 "github.com/openshift/api/route/v1"
|
|
"github.com/pkg/errors"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
v1 "k8s.io/api/core/v1"
|
|
networkingv1 "k8s.io/api/networking/v1"
|
|
storagev1 "k8s.io/api/storage/v1"
|
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/record"
|
|
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller"
|
|
"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"
|
|
|
|
cdiv1 "kubevirt.io/containerized-data-importer-api/pkg/apis/core/v1beta1"
|
|
"kubevirt.io/containerized-data-importer/pkg/common"
|
|
cc "kubevirt.io/containerized-data-importer/pkg/controller/common"
|
|
"kubevirt.io/containerized-data-importer/pkg/operator"
|
|
"kubevirt.io/containerized-data-importer/pkg/util"
|
|
"kubevirt.io/containerized-data-importer/pkg/util/cert"
|
|
)
|
|
|
|
// AnnConfigAuthority is the annotation specifying a resource as the CDIConfig authority
|
|
const (
|
|
AnnConfigAuthority = "cdi.kubevirt.io/configAuthority"
|
|
|
|
errResourceDoesntExist = "ErrResourceDoesntExist"
|
|
messageResourceDoesntExist = "Resource managed by %q doesn't exist"
|
|
|
|
defaultCPULimit = "750m"
|
|
defaultMemLimit = "600M"
|
|
defaultCPURequest = "100m"
|
|
defaultMemRequest = "60M"
|
|
|
|
rootCertificateConfigMap = "kube-root-ca.crt"
|
|
)
|
|
|
|
// CDIConfigReconciler members
|
|
type CDIConfigReconciler struct {
|
|
client client.Client
|
|
// use this for getting any resources not in the install namespace or cluster scope
|
|
uncachedClient client.Client
|
|
recorder record.EventRecorder
|
|
scheme *runtime.Scheme
|
|
log logr.Logger
|
|
uploadProxyServiceName string
|
|
configName string
|
|
cdiNamespace string
|
|
installerLabels map[string]string
|
|
}
|
|
|
|
// Reconcile the reconcile loop for the CDIConfig object.
|
|
func (r *CDIConfigReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
|
|
log := r.log.WithValues("CDIConfig", req.NamespacedName)
|
|
log.Info("reconciling CDIConfig")
|
|
|
|
config, err := r.createCDIConfig()
|
|
if err != nil {
|
|
log.Error(err, "Unable to create CDIConfig")
|
|
return reconcile.Result{}, err
|
|
}
|
|
// Keep a copy of the original for comparison later.
|
|
currentConfigCopy := config.DeepCopyObject()
|
|
|
|
config.Status.Preallocation = config.Spec.Preallocation != nil && *config.Spec.Preallocation
|
|
|
|
// ignore whatever is in config spec and set to operator view
|
|
if err := r.setOperatorParams(config); err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
if err := r.reconcileUploadProxy(config); err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
if err := r.reconcileStorageClass(config); err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
if err := r.reconcileDefaultPodResourceRequirements(config); err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
if err := r.reconcileImagePullSecrets(config); err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
if err := r.reconcileFilesystemOverhead(config); err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
if err := r.reconcileImportProxy(config); err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
|
|
if !reflect.DeepEqual(currentConfigCopy, config) {
|
|
// Updates have happened, update CDIConfig.
|
|
log.Info("Updating CDIConfig", "CDIConfig.Name", config.Name, "config", config)
|
|
if err := r.client.Update(context.TODO(), config); err != nil {
|
|
return reconcile.Result{}, err
|
|
}
|
|
}
|
|
|
|
return reconcile.Result{}, nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) setOperatorParams(config *cdiv1.CDIConfig) error {
|
|
util.SetRecommendedLabels(config, r.installerLabels, "cdi-controller")
|
|
|
|
cdiCR, err := cc.GetActiveCDI(context.TODO(), r.client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cdiCR == nil {
|
|
return nil
|
|
}
|
|
|
|
if _, ok := cdiCR.Annotations[AnnConfigAuthority]; !ok {
|
|
return nil
|
|
}
|
|
|
|
if cdiCR.Spec.Config == nil {
|
|
config.Spec = cdiv1.CDIConfigSpec{}
|
|
} else {
|
|
config.Spec = *cdiCR.Spec.Config
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileUploadProxy(config *cdiv1.CDIConfig) error {
|
|
log := r.log.WithName("CDIconfig").WithName("UploadProxyReconcile")
|
|
config.Status.UploadProxyURL = config.Spec.UploadProxyURLOverride
|
|
// No override, try Ingress
|
|
if config.Status.UploadProxyURL == nil {
|
|
ingress, err := r.reconcileIngress(config)
|
|
if err != nil {
|
|
log.Error(err, "Unable to reconcile Ingress")
|
|
return err
|
|
}
|
|
|
|
if ingress != nil {
|
|
if err := r.reconcileUploadProxyIngressCA(config, *ingress); err != nil {
|
|
log.Error(err, "Unable to reconcile Ingress CA")
|
|
return fmt.Errorf("unable to reconcile Ingress CA: %w", err)
|
|
}
|
|
}
|
|
}
|
|
// No override or Ingress, try Route
|
|
if config.Status.UploadProxyURL == nil {
|
|
if err := r.reconcileRoute(config); err != nil {
|
|
log.Error(err, "Unable to reconcile Routes")
|
|
return err
|
|
}
|
|
|
|
if err := r.reconcileUploadProxyRouteCA(config); err != nil {
|
|
log.Error(err, "Unable to reconcile Route CA")
|
|
return fmt.Errorf("unable to reconcile Route CA: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileUploadProxyIngressCA(config *cdiv1.CDIConfig, ingress networkingv1.Ingress) error {
|
|
log := r.log.WithName("CDIconfig").WithName("UploadProxyIngressCAReconcile")
|
|
|
|
url := config.Status.UploadProxyURL
|
|
if url == nil || *url == "" {
|
|
return nil
|
|
}
|
|
|
|
var secretName string
|
|
i := slices.IndexFunc(ingress.Spec.TLS, func(tls networkingv1.IngressTLS) bool { return tls.SecretName != "" })
|
|
if i == -1 {
|
|
log.Info("Secret name not found in Ingress")
|
|
config.Status.UploadProxyCA = nil
|
|
return nil
|
|
}
|
|
secretName = ingress.Spec.TLS[i].SecretName
|
|
|
|
var secret corev1.Secret
|
|
err := r.client.Get(context.TODO(), types.NamespacedName{Name: secretName, Namespace: r.cdiNamespace}, &secret)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get secret %q: %v", secretName, err)
|
|
}
|
|
|
|
certBytes, ok := secret.Data["tls.crt"]
|
|
if !ok {
|
|
log.Info(fmt.Sprintf("Secret %q does not contain %q", secretName, "tls.crt"))
|
|
config.Status.UploadProxyCA = nil
|
|
return nil
|
|
}
|
|
|
|
certs, err := cert.ParseCertsPEM(certBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse tls.crt: %v", err)
|
|
}
|
|
|
|
s, err := findCertByHostName(*config.Status.UploadProxyURL, certs)
|
|
if err != nil {
|
|
return err
|
|
} else if s == "" {
|
|
log.Info("No matching valid certificate found for upload proxy URL", "UploadProxyURL", *config.Status.UploadProxyURL)
|
|
config.Status.UploadProxyCA = nil
|
|
return nil
|
|
}
|
|
|
|
log.Info("Setting upload proxy CA", "UploadProxyCA", s)
|
|
config.Status.UploadProxyCA = &s
|
|
return nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileUploadProxyRouteCA(config *cdiv1.CDIConfig) error {
|
|
log := r.log.WithName("CDIconfig").WithName("UploadProxyRouteCAReconcile")
|
|
|
|
if config.Status.UploadProxyURL == nil || *config.Status.UploadProxyURL == "" {
|
|
log.Info("No upload proxy URL found, setting upload proxy CA to blank")
|
|
config.Status.UploadProxyCA = nil
|
|
return nil
|
|
}
|
|
|
|
var cm corev1.ConfigMap
|
|
err := r.client.Get(context.TODO(), types.NamespacedName{Name: rootCertificateConfigMap, Namespace: r.cdiNamespace}, &cm)
|
|
if err != nil {
|
|
log.Info(fmt.Sprintf("Could not get certificates: %v", err))
|
|
config.Status.UploadProxyCA = nil
|
|
return nil
|
|
}
|
|
|
|
rawCert, ok := cm.Data["ca.crt"]
|
|
if !ok {
|
|
log.Info(fmt.Sprintf("Config map %q does not contain %q", rootCertificateConfigMap, "ca.crt"))
|
|
config.Status.UploadProxyCA = nil
|
|
return nil
|
|
}
|
|
|
|
certs, err := cert.ParseCertsPEM([]byte(rawCert))
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse ca.crt: %v", err)
|
|
}
|
|
|
|
s, err := findCertByHostName(*config.Status.UploadProxyURL, certs)
|
|
if err != nil {
|
|
return err
|
|
} else if s == "" {
|
|
log.Info("No matching valid certificate found for upload proxy URL", "UploadProxyURL", *config.Status.UploadProxyURL)
|
|
config.Status.UploadProxyCA = nil
|
|
return nil
|
|
}
|
|
|
|
log.Info("Setting upload proxy CA", "UploadProxyCA", s)
|
|
config.Status.UploadProxyCA = &s
|
|
return nil
|
|
}
|
|
|
|
func findCertByHostName(hostName string, certs []*x509.Certificate) (string, error) {
|
|
now := time.Now()
|
|
var latestValidCert *x509.Certificate
|
|
for _, cert := range certs {
|
|
// Check validity
|
|
if now.After(cert.NotAfter) {
|
|
continue
|
|
}
|
|
if now.Before(cert.NotBefore) {
|
|
continue
|
|
}
|
|
if err := cert.VerifyHostname(hostName); err != nil {
|
|
continue
|
|
}
|
|
|
|
// Check if this is the cert with the latest expiration date
|
|
if latestValidCert == nil {
|
|
latestValidCert = cert
|
|
continue
|
|
}
|
|
if latestValidCert.NotAfter.After(cert.NotAfter) {
|
|
continue
|
|
}
|
|
latestValidCert = cert
|
|
}
|
|
|
|
if latestValidCert != nil {
|
|
return buildPemFromCert(latestValidCert, certs)
|
|
}
|
|
|
|
if len(certs) > 0 {
|
|
return buildPemFromAllCerts(certs)
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func buildPemFromCert(matchingCert *x509.Certificate, allCerts []*x509.Certificate) (string, error) {
|
|
pemOut := strings.Builder{}
|
|
|
|
if err := pem.Encode(&pemOut, &pem.Block{Type: "CERTIFICATE", Bytes: matchingCert.Raw}); err != nil {
|
|
return "", fmt.Errorf("could not encode certificate: %w", err)
|
|
}
|
|
|
|
if matchingCert.Issuer.CommonName != matchingCert.Subject.CommonName && !matchingCert.IsCA {
|
|
//lookup issuer recursively, if not found a blank is returned.
|
|
chain, err := findCertByHostName(matchingCert.Issuer.CommonName, allCerts)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if _, err := pemOut.WriteString(chain); err != nil {
|
|
return "", fmt.Errorf("could not write issuer certificate: %w", err)
|
|
}
|
|
}
|
|
|
|
return strings.TrimSpace(pemOut.String()), nil
|
|
}
|
|
|
|
func buildPemFromAllCerts(allCerts []*x509.Certificate) (string, error) {
|
|
now := time.Now()
|
|
pemOut := strings.Builder{}
|
|
for _, cert := range allCerts {
|
|
if now.After(cert.NotAfter) {
|
|
continue
|
|
}
|
|
|
|
if now.Before(cert.NotBefore) {
|
|
continue
|
|
}
|
|
|
|
if err := pem.Encode(&pemOut, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
|
|
return "", fmt.Errorf("could not encode certificate: %w", err)
|
|
}
|
|
}
|
|
|
|
return strings.TrimSpace(pemOut.String()), nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileIngress(config *cdiv1.CDIConfig) (*networkingv1.Ingress, error) {
|
|
log := r.log.WithName("CDIconfig").WithName("IngressReconcile")
|
|
ingressList := &networkingv1.IngressList{}
|
|
if err := r.client.List(context.TODO(), ingressList, &client.ListOptions{Namespace: r.cdiNamespace}); cc.IgnoreIsNoMatchError(err) != nil {
|
|
return nil, err
|
|
}
|
|
for _, ingress := range ingressList.Items {
|
|
ingressURL := getURLFromIngress(&ingress, r.uploadProxyServiceName)
|
|
if ingressURL != "" {
|
|
log.Info("Setting upload proxy url", "IngressURL", ingressURL)
|
|
config.Status.UploadProxyURL = &ingressURL
|
|
return &ingress, nil
|
|
}
|
|
}
|
|
log.Info("No ingress found, setting to blank", "IngressURL", "")
|
|
config.Status.UploadProxyURL = nil
|
|
return nil, nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileRoute(config *cdiv1.CDIConfig) error {
|
|
log := r.log.WithName("CDIconfig").WithName("RouteReconcile")
|
|
routeList := &routev1.RouteList{}
|
|
if err := r.client.List(context.TODO(), routeList, &client.ListOptions{Namespace: r.cdiNamespace}); cc.IgnoreIsNoMatchError(err) != nil {
|
|
return err
|
|
}
|
|
for _, route := range routeList.Items {
|
|
routeURL := getURLFromRoute(&route, r.uploadProxyServiceName)
|
|
if routeURL != "" {
|
|
log.Info("Setting upload proxy url", "RouteURL", routeURL)
|
|
config.Status.UploadProxyURL = &routeURL
|
|
return nil
|
|
}
|
|
}
|
|
log.Info("No route found, setting to blank", "RouteURL", "")
|
|
config.Status.UploadProxyURL = nil
|
|
return nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileStorageClass(config *cdiv1.CDIConfig) error {
|
|
log := r.log.WithName("CDIconfig").WithName("StorageClassReconcile")
|
|
storageClassList := &storagev1.StorageClassList{}
|
|
if err := r.client.List(context.TODO(), storageClassList, &client.ListOptions{}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check config for scratch space class
|
|
if config.Spec.ScratchSpaceStorageClass != nil {
|
|
for _, storageClass := range storageClassList.Items {
|
|
if storageClass.Name == *config.Spec.ScratchSpaceStorageClass {
|
|
log.Info("Setting scratch space to override", "storageClass.Name", storageClass.Name)
|
|
config.Status.ScratchSpaceStorageClass = storageClass.Name
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
// Check for default storage class.
|
|
for _, storageClass := range storageClassList.Items {
|
|
if defaultClassValue, ok := storageClass.Annotations[cc.AnnDefaultStorageClass]; ok {
|
|
if defaultClassValue == "true" {
|
|
log.Info("Setting scratch space to default", "storageClass.Name", storageClass.Name)
|
|
config.Status.ScratchSpaceStorageClass = storageClass.Name
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
log.Info("No default storage class found, setting scratch space to blank")
|
|
// No storage class found, blank it out.
|
|
config.Status.ScratchSpaceStorageClass = ""
|
|
return nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileImagePullSecrets(config *cdiv1.CDIConfig) error {
|
|
config.Status.ImagePullSecrets = config.Spec.ImagePullSecrets
|
|
return nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileDefaultPodResourceRequirements(config *cdiv1.CDIConfig) error {
|
|
cpuLimit, _ := resource.ParseQuantity(defaultCPULimit)
|
|
memLimit, _ := resource.ParseQuantity(defaultMemLimit)
|
|
cpuRequest, _ := resource.ParseQuantity(defaultCPURequest)
|
|
memRequest, _ := resource.ParseQuantity(defaultMemRequest)
|
|
config.Status.DefaultPodResourceRequirements = &v1.ResourceRequirements{
|
|
Limits: map[v1.ResourceName]resource.Quantity{
|
|
v1.ResourceCPU: cpuLimit,
|
|
v1.ResourceMemory: memLimit,
|
|
},
|
|
Requests: map[v1.ResourceName]resource.Quantity{
|
|
v1.ResourceCPU: cpuRequest,
|
|
v1.ResourceMemory: memRequest,
|
|
},
|
|
}
|
|
|
|
if config.Spec.PodResourceRequirements != nil {
|
|
if config.Spec.PodResourceRequirements.Limits != nil {
|
|
if cpu, exist := config.Spec.PodResourceRequirements.Limits[v1.ResourceCPU]; exist {
|
|
config.Status.DefaultPodResourceRequirements.Limits[v1.ResourceCPU] = cpu
|
|
}
|
|
|
|
if memory, exist := config.Spec.PodResourceRequirements.Limits[v1.ResourceMemory]; exist {
|
|
config.Status.DefaultPodResourceRequirements.Limits[v1.ResourceMemory] = memory
|
|
}
|
|
}
|
|
|
|
if config.Spec.PodResourceRequirements.Requests != nil {
|
|
if cpu, exist := config.Spec.PodResourceRequirements.Requests[v1.ResourceCPU]; exist {
|
|
config.Status.DefaultPodResourceRequirements.Requests[v1.ResourceCPU] = cpu
|
|
}
|
|
|
|
if memory, exist := config.Spec.PodResourceRequirements.Requests[v1.ResourceMemory]; exist {
|
|
config.Status.DefaultPodResourceRequirements.Requests[v1.ResourceMemory] = memory
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileFilesystemOverhead(config *cdiv1.CDIConfig) error {
|
|
var globalOverhead cdiv1.Percent = common.DefaultGlobalOverhead
|
|
var perStorageConfig = make(map[string]cdiv1.Percent)
|
|
|
|
log := r.log.WithName("CDIconfig").WithName("FilesystemOverhead")
|
|
|
|
// Avoid nil maps and segfaults for the initial case, where filesystemOverhead
|
|
// is nil for both the spec and the status.
|
|
if config.Status.FilesystemOverhead == nil {
|
|
log.Info("No filesystem overhead found in status, initializing to defaults")
|
|
config.Status.FilesystemOverhead = &cdiv1.FilesystemOverhead{
|
|
Global: globalOverhead,
|
|
StorageClass: make(map[string]cdiv1.Percent),
|
|
}
|
|
}
|
|
|
|
if config.Spec.FilesystemOverhead != nil {
|
|
if valid, _ := validOverhead(config.Spec.FilesystemOverhead.Global); valid {
|
|
globalOverhead = config.Spec.FilesystemOverhead.Global
|
|
}
|
|
if config.Spec.FilesystemOverhead.StorageClass != nil {
|
|
perStorageConfig = config.Spec.FilesystemOverhead.StorageClass
|
|
}
|
|
}
|
|
|
|
// Set status global overhead
|
|
config.Status.FilesystemOverhead.Global = globalOverhead
|
|
|
|
// Set status per-storageClass overhead
|
|
storageClassList := &storagev1.StorageClassList{}
|
|
if err := r.client.List(context.TODO(), storageClassList, &client.ListOptions{}); err != nil {
|
|
return err
|
|
}
|
|
config.Status.FilesystemOverhead.StorageClass = make(map[string]cdiv1.Percent)
|
|
for _, storageClass := range storageClassList.Items {
|
|
storageClassName := storageClass.GetName()
|
|
storageClassNameOverhead, found := perStorageConfig[storageClassName]
|
|
|
|
if found {
|
|
valid, err := validOverhead(storageClassNameOverhead)
|
|
if !valid {
|
|
return err
|
|
}
|
|
config.Status.FilesystemOverhead.StorageClass[storageClassName] = storageClassNameOverhead
|
|
} else {
|
|
config.Status.FilesystemOverhead.StorageClass[storageClassName] = globalOverhead
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validOverhead(overhead cdiv1.Percent) (bool, error) {
|
|
return regexp.MatchString(`^(0(?:\.\d{1,3})?|1)$`, string(overhead))
|
|
}
|
|
|
|
// createCDIConfig creates a new instance of the CDIConfig object if it doesn't exist already, and returns the existing one if found.
|
|
// It also sets the operator to be the owner of the CDIConfig object.
|
|
func (r *CDIConfigReconciler) createCDIConfig() (*cdiv1.CDIConfig, error) {
|
|
config := &cdiv1.CDIConfig{}
|
|
if err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: r.configName}, config); err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
config = cc.MakeEmptyCDIConfigSpec(r.configName)
|
|
if err := operator.SetOwnerRuntime(r.uncachedClient, config); err != nil {
|
|
return nil, err
|
|
}
|
|
util.SetRecommendedLabels(config, r.installerLabels, "cdi-controller")
|
|
if err := r.client.Create(context.TODO(), config); err != nil {
|
|
if k8serrors.IsAlreadyExists(err) {
|
|
config := &cdiv1.CDIConfig{}
|
|
if err := r.uncachedClient.Get(context.TODO(), types.NamespacedName{Name: r.configName}, config); err == nil {
|
|
return config, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) reconcileImportProxy(config *cdiv1.CDIConfig) error {
|
|
config.Status.ImportProxy = config.Spec.ImportProxy
|
|
|
|
// Avoid nil pointers and segfaults for the initial case, where ImportProxy is nil for both the spec and the status.
|
|
if config.Status.ImportProxy == nil {
|
|
config.Status.ImportProxy = &cdiv1.ImportProxy{
|
|
HTTPProxy: new(string),
|
|
HTTPSProxy: new(string),
|
|
NoProxy: new(string),
|
|
TrustedCAProxy: new(string),
|
|
}
|
|
|
|
// Try Openshift cluster wide proxy only if the CDIConfig default config is empty
|
|
clusterWideProxy, err := getClusterWideProxy(r.client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.Status.ImportProxy.HTTPProxy = &clusterWideProxy.Status.HTTPProxy
|
|
config.Status.ImportProxy.HTTPSProxy = &clusterWideProxy.Status.HTTPSProxy
|
|
config.Status.ImportProxy.NoProxy = &clusterWideProxy.Status.NoProxy
|
|
if err := r.reconcileImportProxyCAConfigMap(config, clusterWideProxy); err != nil {
|
|
return err
|
|
}
|
|
config.Status.ImportProxy.TrustedCAProxy = &clusterWideProxy.Spec.TrustedCA.Name
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create/Update a configmap with the CA certificates in the controllor context with the cluster-wide proxy CA certificates to be used by the importer pod
|
|
func (r *CDIConfigReconciler) reconcileImportProxyCAConfigMap(config *cdiv1.CDIConfig, clusterWideProxy *ocpconfigv1.Proxy) error {
|
|
cmOldName := config.Status.ImportProxy.TrustedCAProxy
|
|
cmName := clusterWideProxy.Spec.TrustedCA.Name
|
|
client := r.uncachedClient
|
|
|
|
// Delete old ConfigMap if name changed
|
|
if cmOldName != nil && *cmOldName != "" && *cmOldName != cmName {
|
|
if err := client.Delete(context.TODO(), r.createProxyConfigMap(*cmOldName, "")); err != nil && !k8serrors.IsNotFound(err) {
|
|
return err
|
|
}
|
|
}
|
|
if cmName == "" {
|
|
return nil
|
|
}
|
|
|
|
clusterWideProxyConfigMap := &v1.ConfigMap{}
|
|
if err := client.Get(context.TODO(), types.NamespacedName{Name: cmName, Namespace: ClusterWideProxyConfigMapNameSpace}, clusterWideProxyConfigMap); err != nil {
|
|
if k8serrors.IsNotFound(err) {
|
|
msg := fmt.Sprintf(messageResourceDoesntExist, cmName)
|
|
r.recorder.Event(clusterWideProxy, v1.EventTypeWarning, errResourceDoesntExist, msg)
|
|
}
|
|
return err
|
|
}
|
|
// Copy the cluster-wide proxy CA certificates to the importer pod proxy CA certificates configMap
|
|
certBytes, ok := clusterWideProxyConfigMap.Data[ClusterWideProxyConfigMapKey]
|
|
if !ok {
|
|
return fmt.Errorf("no cluster-wide proxy CA certificate")
|
|
}
|
|
configMap := &v1.ConfigMap{}
|
|
if err := client.Get(context.TODO(), types.NamespacedName{Name: cmName, Namespace: r.cdiNamespace}, configMap); err != nil {
|
|
if !k8serrors.IsNotFound(err) {
|
|
return err
|
|
}
|
|
proxyConfigMap := r.createProxyConfigMap(cmName, certBytes)
|
|
util.SetRecommendedLabels(proxyConfigMap, r.installerLabels, "cdi-controller")
|
|
if err := client.Create(context.TODO(), proxyConfigMap); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
configMap.Data[common.ImportProxyConfigMapKey] = certBytes
|
|
util.SetRecommendedLabels(configMap, r.installerLabels, "cdi-controller")
|
|
if err := client.Update(context.TODO(), configMap); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *CDIConfigReconciler) createProxyConfigMap(cmName, cert string) *v1.ConfigMap {
|
|
return &v1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: cmName,
|
|
Namespace: r.cdiNamespace},
|
|
Data: map[string]string{common.ImportProxyConfigMapKey: cert},
|
|
}
|
|
}
|
|
|
|
// Init initializes a CDIConfig object.
|
|
func (r *CDIConfigReconciler) Init() error {
|
|
_, err := r.createCDIConfig()
|
|
return err
|
|
}
|
|
|
|
// NewConfigController creates a new instance of the config controller.
|
|
func NewConfigController(mgr manager.Manager, log logr.Logger, uploadProxyServiceName, configName string, installerLabels map[string]string) (controller.Controller, error) {
|
|
uncachedClient, err := client.New(mgr.GetConfig(), client.Options{
|
|
Scheme: mgr.GetScheme(),
|
|
Mapper: mgr.GetRESTMapper(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reconciler := &CDIConfigReconciler{
|
|
client: mgr.GetClient(),
|
|
uncachedClient: uncachedClient,
|
|
recorder: mgr.GetEventRecorderFor("config-controller"),
|
|
scheme: mgr.GetScheme(),
|
|
log: log.WithName("config-controller"),
|
|
uploadProxyServiceName: uploadProxyServiceName,
|
|
configName: configName,
|
|
cdiNamespace: util.GetNamespace(),
|
|
installerLabels: installerLabels,
|
|
}
|
|
|
|
configController, err := controller.New("config-controller", mgr, controller.Options{
|
|
MaxConcurrentReconciles: 3,
|
|
Reconciler: reconciler,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := addConfigControllerWatches(mgr, configController, reconciler.cdiNamespace, configName, uploadProxyServiceName, log); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := reconciler.Init(); err != nil {
|
|
log.Error(err, "Unable to initialize CDIConfig")
|
|
}
|
|
log.Info("Initialized CDI Config object")
|
|
return configController, nil
|
|
}
|
|
|
|
// addConfigControllerWatches sets up the watches used by the config controller.
|
|
func addConfigControllerWatches(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string, log logr.Logger) error {
|
|
// Setup watches
|
|
if err := watchCDIConfig(mgr, configController, configName); err != nil {
|
|
return err
|
|
}
|
|
if err := watchStorageClass(mgr, configController, configName); err != nil {
|
|
return err
|
|
}
|
|
if err := watchIngress(mgr, configController, cdiNamespace, configName, uploadProxyServiceName); err != nil {
|
|
return err
|
|
}
|
|
if err := watchRoutes(mgr, configController, cdiNamespace, configName, uploadProxyServiceName); err != nil {
|
|
return err
|
|
}
|
|
if err := watchClusterProxy(mgr, configController, configName); err != nil {
|
|
return err
|
|
}
|
|
if err := watchUploadProxyCA(mgr, configController, configName); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func watchCDIConfig(mgr manager.Manager, configController controller.Controller, configName string) error {
|
|
if err := configController.Watch(source.Kind(mgr.GetCache(), &cdiv1.CDIConfig{}, &handler.TypedEnqueueRequestForObject[*cdiv1.CDIConfig]{})); err != nil {
|
|
return err
|
|
}
|
|
return configController.Watch(source.Kind(mgr.GetCache(), &cdiv1.CDI{}, handler.TypedEnqueueRequestsFromMapFunc[*cdiv1.CDI](
|
|
func(_ context.Context, _ *cdiv1.CDI) []reconcile.Request {
|
|
return []reconcile.Request{{
|
|
NamespacedName: types.NamespacedName{Name: configName},
|
|
}}
|
|
},
|
|
)))
|
|
}
|
|
|
|
func watchStorageClass(mgr manager.Manager, configController controller.Controller, configName string) error {
|
|
return configController.Watch(source.Kind(mgr.GetCache(), &storagev1.StorageClass{}, handler.TypedEnqueueRequestsFromMapFunc[*storagev1.StorageClass](
|
|
func(_ context.Context, _ *storagev1.StorageClass) []reconcile.Request {
|
|
return []reconcile.Request{{
|
|
NamespacedName: types.NamespacedName{Name: configName},
|
|
}}
|
|
},
|
|
)))
|
|
}
|
|
|
|
func watchIngress(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string) error {
|
|
err := configController.Watch(source.Kind(mgr.GetCache(), &networkingv1.Ingress{}, handler.TypedEnqueueRequestsFromMapFunc[*networkingv1.Ingress](
|
|
func(_ context.Context, _ *networkingv1.Ingress) []reconcile.Request {
|
|
return []reconcile.Request{{
|
|
NamespacedName: types.NamespacedName{Name: configName},
|
|
}}
|
|
}),
|
|
predicate.TypedFuncs[*networkingv1.Ingress]{
|
|
CreateFunc: func(e event.TypedCreateEvent[*networkingv1.Ingress]) bool {
|
|
return "" != getURLFromIngress(e.Object, uploadProxyServiceName) &&
|
|
e.Object.GetNamespace() == cdiNamespace
|
|
},
|
|
UpdateFunc: func(e event.TypedUpdateEvent[*networkingv1.Ingress]) bool {
|
|
return "" != getURLFromIngress(e.ObjectNew, uploadProxyServiceName) &&
|
|
e.ObjectNew.GetNamespace() == cdiNamespace
|
|
},
|
|
DeleteFunc: func(e event.TypedDeleteEvent[*networkingv1.Ingress]) bool {
|
|
return "" != getURLFromIngress(e.Object, uploadProxyServiceName) &&
|
|
e.Object.GetNamespace() == cdiNamespace
|
|
},
|
|
}))
|
|
return err
|
|
}
|
|
|
|
// we only watch the route obj if they exist, i.e., if it is an OpenShift cluster
|
|
func watchRoutes(mgr manager.Manager, configController controller.Controller, cdiNamespace, configName, uploadProxyServiceName string) error {
|
|
err := mgr.GetClient().List(context.TODO(), &routev1.RouteList{}, &client.ListOptions{Namespace: cdiNamespace})
|
|
if !meta.IsNoMatchError(err) {
|
|
if err == nil || cc.IsErrCacheNotStarted(err) {
|
|
err := configController.Watch(source.Kind(mgr.GetCache(), &routev1.Route{}, handler.TypedEnqueueRequestsFromMapFunc[*routev1.Route](
|
|
func(_ context.Context, _ *routev1.Route) []reconcile.Request {
|
|
return []reconcile.Request{{
|
|
NamespacedName: types.NamespacedName{Name: configName},
|
|
}}
|
|
}),
|
|
predicate.TypedFuncs[*routev1.Route]{
|
|
CreateFunc: func(e event.TypedCreateEvent[*routev1.Route]) bool {
|
|
return "" != getURLFromRoute(e.Object, uploadProxyServiceName) &&
|
|
e.Object.GetNamespace() == cdiNamespace
|
|
},
|
|
UpdateFunc: func(e event.TypedUpdateEvent[*routev1.Route]) bool {
|
|
return "" != getURLFromRoute(e.ObjectNew, uploadProxyServiceName) &&
|
|
e.ObjectNew.GetNamespace() == cdiNamespace
|
|
},
|
|
DeleteFunc: func(e event.TypedDeleteEvent[*routev1.Route]) bool {
|
|
return "" != getURLFromRoute(e.Object, uploadProxyServiceName) &&
|
|
e.Object.GetNamespace() == cdiNamespace
|
|
},
|
|
}))
|
|
return err
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// we only watch the cluster-wide proxy obj if they exist, i.e., if it is an OpenShift cluster
|
|
func watchClusterProxy(mgr manager.Manager, configController controller.Controller, configName string) error {
|
|
err := mgr.GetClient().List(context.TODO(), &ocpconfigv1.ProxyList{})
|
|
if !meta.IsNoMatchError(err) {
|
|
if err == nil || cc.IsErrCacheNotStarted(err) {
|
|
return configController.Watch(source.Kind(mgr.GetCache(), &ocpconfigv1.Proxy{}, handler.TypedEnqueueRequestsFromMapFunc[*ocpconfigv1.Proxy](
|
|
func(_ context.Context, _ *ocpconfigv1.Proxy) []reconcile.Request {
|
|
return []reconcile.Request{{
|
|
NamespacedName: types.NamespacedName{Name: configName},
|
|
}}
|
|
},
|
|
)))
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// watchUploadProxyCA watches the kube-root-ca.crt ConfigMap for changes
|
|
// to the CA certificate used by the upload proxy.
|
|
//
|
|
// A change in the UploadProxyURL may invalidate the CA certificate, but
|
|
// watchCDIConfig will handle that.
|
|
func watchUploadProxyCA(mgr manager.Manager, configcontroller controller.Controller, configName string) error {
|
|
handler := handler.TypedEnqueueRequestsFromMapFunc[*v1.ConfigMap](func(context.Context, *v1.ConfigMap) []reconcile.Request {
|
|
return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: configName}}}
|
|
})
|
|
|
|
predicate := predicate.NewTypedPredicateFuncs[*v1.ConfigMap](func(o *v1.ConfigMap) bool {
|
|
return o.Name == rootCertificateConfigMap
|
|
})
|
|
|
|
if err := configcontroller.Watch(source.Kind(mgr.GetCache(), &v1.ConfigMap{}, handler, predicate)); err != nil {
|
|
return fmt.Errorf("could not watch UploadProxyCA ConfigMap: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getURLFromIngress(ing *networkingv1.Ingress, uploadProxyServiceName string) string {
|
|
if ing.Spec.DefaultBackend != nil && ing.Spec.DefaultBackend.Service != nil {
|
|
if ing.Spec.DefaultBackend.Service.Name != uploadProxyServiceName {
|
|
return ""
|
|
}
|
|
return ing.Spec.Rules[0].Host
|
|
}
|
|
for _, rule := range ing.Spec.Rules {
|
|
if rule.HTTP == nil {
|
|
continue
|
|
}
|
|
for _, path := range rule.HTTP.Paths {
|
|
if path.Backend.Service != nil && path.Backend.Service.Name == uploadProxyServiceName {
|
|
if rule.Host != "" {
|
|
return rule.Host
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getURLFromRoute(route *routev1.Route, uploadProxyServiceName string) string {
|
|
if route.Spec.To.Name == uploadProxyServiceName {
|
|
if len(route.Status.Ingress) > 0 {
|
|
return route.Status.Ingress[0].Host
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// getClusterWideProxy returns the OpenShift cluster wide proxy object
|
|
func getClusterWideProxy(r client.Client) (*ocpconfigv1.Proxy, error) {
|
|
clusterWideProxy := &ocpconfigv1.Proxy{}
|
|
// Ignore both no CRD found (IgnoreIsNoMatch) and the object itself not existing IsNotFound because we want to skip if not
|
|
// in Open Shift.
|
|
if err := r.Get(context.TODO(), types.NamespacedName{Name: ClusterWideProxyName}, clusterWideProxy); cc.IgnoreIsNoMatchError(err) != nil && !k8serrors.IsNotFound(err) {
|
|
return nil, err
|
|
}
|
|
return clusterWideProxy, nil
|
|
}
|
|
|
|
// GetImportProxyConfig attempts to import proxy URLs if configured in the CDIConfig.
|
|
func GetImportProxyConfig(config *cdiv1.CDIConfig, field string) (string, error) {
|
|
if config == nil {
|
|
return "", errors.New("failed to get field, the CDIConfig is nil")
|
|
}
|
|
if config.Status.ImportProxy == nil {
|
|
return "", errors.New("failed to get field, the CDIConfig ImportProxy is nil")
|
|
}
|
|
|
|
switch field {
|
|
case common.ImportProxyHTTP:
|
|
if config.Status.ImportProxy.HTTPProxy != nil {
|
|
return *config.Status.ImportProxy.HTTPProxy, nil
|
|
}
|
|
case common.ImportProxyHTTPS:
|
|
if config.Status.ImportProxy.HTTPSProxy != nil {
|
|
return *config.Status.ImportProxy.HTTPSProxy, nil
|
|
}
|
|
case common.ImportProxyNoProxy:
|
|
if config.Status.ImportProxy.NoProxy != nil {
|
|
return *config.Status.ImportProxy.NoProxy, nil
|
|
}
|
|
case common.ImportProxyConfigMapName:
|
|
if config.Status.ImportProxy.TrustedCAProxy != nil {
|
|
return *config.Status.ImportProxy.TrustedCAProxy, nil
|
|
}
|
|
default:
|
|
return "", errors.Errorf("CDIConfig ImportProxy does not have the field: %s", field)
|
|
}
|
|
|
|
// If everything fails, return blank
|
|
return "", nil
|
|
}
|