diff --git a/internal/controller/constants.go b/internal/controller/constants.go index 9bb1ba1de..1a5db436e 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -28,6 +28,8 @@ const ( /*** application server configuration file ***/ // OLSConfigName is the name of the OLSConfig configmap OLSConfigCmName = "olsconfig" + // AppServerConfigCmName is the name of the app server configmap + AppServerConfigCmName = "olsconfig" // OLSCAConfigMap is the name of the OLS TLS ca certificate configmap OLSCAConfigMap = "openshift-service-ca.crt" // OLSNamespaceDefault is the default namespace for OLS @@ -285,4 +287,6 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt' MCPHeadersMountRoot = "/etc/mcp/headers" // Header Secret Data Path MCPSECRETDATAPATH = "header" + // LSCAppServerActivatorCmName is the name of the LSC app server activator configmap + LSCAppServerActivatorCmName = "lsc-app-server-activator" ) diff --git a/internal/controller/errors.go b/internal/controller/errors.go index cc98cebe7..6a44758ae 100644 --- a/internal/controller/errors.go +++ b/internal/controller/errors.go @@ -68,6 +68,7 @@ const ( ErrGetConsolePluginDeployment = "failed to get Console Plugin deployment" ErrGetConsolePluginNetworkPolicy = "failed to get Console Plugin network policy" ErrGetConsolePluginService = "failed to get Console Plugin service" + ErrGetLSCActivatorConfigmap = "failed to get LSC backend activator configmap" ErrGetLLMSecret = "failed to get LLM provider secret" // #nosec G101 ErrGetOperatorNetworkPolicy = "failed to get operator network policy" ErrGetPostgresNetworkPolicy = "failed to get OLS Postgres network policy" diff --git a/internal/controller/lsc_app_server_assets.go b/internal/controller/lsc_app_server_assets.go new file mode 100644 index 000000000..203ad2c1a --- /dev/null +++ b/internal/controller/lsc_app_server_assets.go @@ -0,0 +1,64 @@ +package controller + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + olsv1alpha1 "github.com/openshift/lightspeed-operator/api/v1alpha1" +) + +// todo: implement LSC config map generation +// +//nolint:unused // Ignore unused lint error before implementation of reconciliation functions +func (r *OLSConfigReconciler) generateLSCConfigMap(ctx context.Context, cr *olsv1alpha1.OLSConfig) (*corev1.ConfigMap, error) { //lint:ignore U1000 Ignore unused lint error before implementation of reconciliation functions + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: AppServerConfigCmName, + Namespace: r.Options.Namespace, + }, + } + return configMap, nil +} + +// todo: implement LSC deployment generation +// +//nolint:unused // Ignore unused lint error before implementation of reconciliation functions +func (r *OLSConfigReconciler) generateLSCDeployment(ctx context.Context, cr *olsv1alpha1.OLSConfig) (*appsv1.Deployment, error) { //lint:ignore U1000 Ignore unused lint error before implementation of reconciliation functions + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: OLSAppServerDeploymentName, + Namespace: r.Options.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: cr.Spec.OLSConfig.DeploymentConfig.Replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: generateAppServerSelectorLabels(), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: generateAppServerSelectorLabels(), + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "lsc-app-server", + Image: r.Options.LightspeedServiceImage, + }, + }, + }, + }, + }, + } + return deployment, nil +} + +// todo: implement LSC deployment update +// +//nolint:unused // Ignore unused lint error before implementation of reconciliation functions +func (r *OLSConfigReconciler) updateLSCDeployment(ctx context.Context, existingDeployment, desiredDeployment *appsv1.Deployment) error { + + return nil +} diff --git a/internal/controller/lsc_app_server_assets_test.go b/internal/controller/lsc_app_server_assets_test.go new file mode 100644 index 000000000..26baac7d4 --- /dev/null +++ b/internal/controller/lsc_app_server_assets_test.go @@ -0,0 +1,78 @@ +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + olsv1alpha1 "github.com/openshift/lightspeed-operator/api/v1alpha1" +) + +var _ = Describe("LSC App server assets", Label("LSCBackend"), Ordered, func() { + var cr *olsv1alpha1.OLSConfig + var r *OLSConfigReconciler + var rOptions *OLSConfigReconcilerOptions + var ctx context.Context + + Context("LSC asset generation", func() { + BeforeEach(func() { + ctx = context.Background() + rOptions = &OLSConfigReconcilerOptions{ + OpenShiftMajor: "123", + OpenshiftMinor: "456", + LightspeedServiceImage: "lightspeed-service:latest", + OpenShiftMCPServerImage: "openshift-mcp-server:latest", + Namespace: OLSNamespaceDefault, + } + cr = getDefaultOLSConfigCR() + r = &OLSConfigReconciler{ + Options: *rOptions, + logger: logf.Log.WithName("olsconfig.reconciler"), + Client: k8sClient, + Scheme: k8sClient.Scheme(), + stateCache: make(map[string]string), + } + }) + + Describe("generateLSCConfigMap", func() { + It("should generate a valid configmap", func() { + cm, err := r.generateLSCConfigMap(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + Expect(cm).NotTo(BeNil()) + }) + + // TODO: Add more tests cases for once implementation is complete + }) + + Describe("generateLSCDeployment", func() { + It("should generate a valid deployment", func() { + deployment, err := r.generateLSCDeployment(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + Expect(deployment).NotTo(BeNil()) + }) + + // TODO: Add more tests cases for once implementation is complete + }) + + Describe("updateLSCDeployment", func() { + var existingDeployment *appsv1.Deployment + var desiredDeployment *appsv1.Deployment + + BeforeEach(func() { + existingDeployment, _ = r.generateLSCDeployment(ctx, cr) + }) + + It("should successfully update deployment", func() { + desiredDeployment, _ = r.generateLSCDeployment(ctx, cr) + err := r.updateLSCDeployment(ctx, existingDeployment, desiredDeployment) + Expect(err).NotTo(HaveOccurred()) + }) + + // TODO: Add more tests cases for once implementation is complete + }) + }) + +}) diff --git a/internal/controller/lsc_app_server_reconciliator.go b/internal/controller/lsc_app_server_reconciliator.go new file mode 100644 index 000000000..cdc6c0749 --- /dev/null +++ b/internal/controller/lsc_app_server_reconciliator.go @@ -0,0 +1,94 @@ +package controller + +import ( + "context" + "fmt" + + olsv1alpha1 "github.com/openshift/lightspeed-operator/api/v1alpha1" +) + +func (r *OLSConfigReconciler) reconcileAppServerLSC(ctx context.Context, olsconfig *olsv1alpha1.OLSConfig) error { + r.logger.Info("reconcileAppServerLSC starts") + tasks := []ReconcileTask{ + { + Name: "reconcile ServiceAccount", + Task: r.reconcileServiceAccount, + }, + { + Name: "reconcile SARRole", + Task: r.reconcileSARRole, + }, + { + Name: "reconcile SARRoleBinding", + Task: r.reconcileSARRoleBinding, + }, + // todo: LSC config map generation + // todo: Llama Stack configmap generation + { + Name: "reconcile OLSConfigMap", + Task: r.reconcileLSCConfigMap, + }, + { + Name: "reconcile Additional CA ConfigMap", + Task: r.reconcileOLSAdditionalCAConfigMap, + }, + { + Name: "reconcile App Service", + Task: r.reconcileService, + }, + { + Name: "reconcile App TLS Certs", + Task: r.reconcileTLSSecret, + }, + // todo: LSC deployment generation + { + Name: "reconcile App Deployment", + Task: r.reconcileLSCDeployment, + }, + { + Name: "reconcile Metrics Reader Secret", + Task: r.reconcileMetricsReaderSecret, + }, + { + Name: "reconcile App ServiceMonitor", + Task: r.reconcileServiceMonitor, + }, + { + Name: "reconcile App PrometheusRule", + Task: r.reconcilePrometheusRule, + }, + { + Name: "reconcile App NetworkPolicy", + Task: r.reconcileAppServerNetworkPolicy, + }, + { + Name: "reconcile Proxy CA ConfigMap", + Task: r.reconcileProxyCAConfigMap, + }, + } + + for _, task := range tasks { + err := task.Task(ctx, olsconfig) + if err != nil { + r.logger.Error(err, "reconcileAppServer error", "task", task.Name) + return fmt.Errorf("failed to %s: %w", task.Name, err) + } + } + + r.logger.Info("reconcileAppServer completes") + + return nil +} + +func (r *OLSConfigReconciler) reconcileLSCConfigMap(ctx context.Context, cr *olsv1alpha1.OLSConfig) error { + + return r.reconcileOLSConfigMap(ctx, cr) + // TODO: implement LSC configmap reconciliation +} + +func (r *OLSConfigReconciler) reconcileLSCDeployment(ctx context.Context, cr *olsv1alpha1.OLSConfig) error { + + return r.reconcileDeployment(ctx, cr) + + // TODO: implement LSC deployment reconciliation +} diff --git a/internal/controller/lsc_app_server_reconciliator_test.go b/internal/controller/lsc_app_server_reconciliator_test.go new file mode 100644 index 000000000..aaac92baa --- /dev/null +++ b/internal/controller/lsc_app_server_reconciliator_test.go @@ -0,0 +1,163 @@ +package controller + +import ( + "reflect" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("LSC App server reconciliator", Label("LSCBackend"), Ordered, func() { + Context("Creation logic", Ordered, func() { + var providerSecret *corev1.Secret + var tlsSecret *corev1.Secret + var configmap *corev1.ConfigMap + var openshiftCertsConfigmap *corev1.ConfigMap + BeforeEach(func() { + By("create magic configmap") + configmap, _ = generateRandomConfigMap() + configmap.Name = LSCAppServerActivatorCmName + configMapCreationErr := reconciler.Create(ctx, configmap) + Expect(configMapCreationErr).NotTo(HaveOccurred()) + By("create the provider secret") + providerSecret, _ = generateRandomSecret() + secretCreationErr := reconciler.Create(ctx, providerSecret) + Expect(secretCreationErr).NotTo(HaveOccurred()) + + By("create the default tls secret") + tlsSecret, _ = generateRandomSecret() + tlsSecret.Name = OLSCertsSecretName + tlsSecret.SetOwnerReferences([]metav1.OwnerReference{ + { + Kind: "Secret", + APIVersion: "v1", + UID: "ownerUID", + Name: OLSCertsSecretName, + }, + }) + secretCreationErr = reconciler.Create(ctx, tlsSecret) + Expect(secretCreationErr).NotTo(HaveOccurred()) + + By("create the Openshift certificates config map") + openshiftCertsConfigmap, _ = generateRandomConfigMap() + openshiftCertsConfigmap.Name = DefaultOpenShiftCerts + configMapCreationErr = reconciler.Create(ctx, openshiftCertsConfigmap) + Expect(configMapCreationErr).NotTo(HaveOccurred()) + + By("Set OLSConfig CR to default") + crDefault := getDefaultOLSConfigCR() + cr.Spec = crDefault.Spec + }) + + AfterEach(func() { + By("Delete the provider secret") + secretDeletionErr := reconciler.Delete(ctx, providerSecret) + Expect(secretDeletionErr).NotTo(HaveOccurred()) + + By("Delete the tls secret") + secretDeletionErr = reconciler.Delete(ctx, tlsSecret) + Expect(secretDeletionErr).NotTo(HaveOccurred()) + + By("Delete the magic configmap") + configMapDeletionErr := reconciler.Delete(ctx, configmap) + Expect(configMapDeletionErr).NotTo(HaveOccurred()) + + By("Delete the Openshift certificates config map") + configMapDeletionErr = reconciler.Delete(ctx, openshiftCertsConfigmap) + Expect(configMapDeletionErr).NotTo(HaveOccurred()) + }) + + It("should call reconcileAppServerLSC when the magic configmap exists", func() { + By("Choose the correct reconcile function") + reconcileFunc, err := reconciler.getAppServerReconcileFunction(ctx) + Expect(err).NotTo(HaveOccurred()) + Expect(reflect.ValueOf(reconcileFunc).Pointer()).To(Equal(reflect.ValueOf(reconciler.reconcileAppServerLSC).Pointer())) + }) + + It("should create a LSC configmap", func() { + By("Reconcile the LSC app server") + err := reconciler.reconcileAppServerLSC(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + + By("Get the config map") + cm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: AppServerConfigCmName, Namespace: OLSNamespaceDefault}, cm) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should create a Llama Stack configmap", func() { + // todo: implement this test after the Llama Stack configmap implementation is complete + Skip("Llama Stack configmap implementation is not complete") + }) + + It("should create a deployment lightspeed-app-server", func() { + By("Reconcile the LSC app server") + err := reconciler.reconcileAppServerLSC(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + + By("Get the deployment") + dep := &appsv1.Deployment{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: OLSAppServerDeploymentName, Namespace: OLSNamespaceDefault}, dep) + Expect(err).NotTo(HaveOccurred()) + }) + + }) + + Context("LSC ConfigMap reconciliation", Ordered, func() { + var providerSecret *corev1.Secret + var openshiftCertsConfigmap *corev1.ConfigMap + BeforeEach(func() { + By("create the provider secret") + providerSecret, _ = generateRandomSecret() + secretCreationErr := reconciler.Create(ctx, providerSecret) + Expect(secretCreationErr).NotTo(HaveOccurred()) + + By("create the tls secret") + tlsSecret, _ = generateRandomSecret() + tlsSecret.Name = OLSCertsSecretName + secretCreationErr = reconciler.Create(ctx, tlsSecret) + Expect(secretCreationErr).NotTo(HaveOccurred()) + + By("create the Openshift certificates config map") + openshiftCertsConfigmap, _ = generateRandomConfigMap() + openshiftCertsConfigmap.Name = DefaultOpenShiftCerts + configMapCreationErr := reconciler.Create(ctx, openshiftCertsConfigmap) + Expect(configMapCreationErr).NotTo(HaveOccurred()) + + By("Set OLSConfig CR to default") + crDefault := getDefaultOLSConfigCR() + cr.Spec = crDefault.Spec + }) + + AfterEach(func() { + By("Delete the provider secret") + secretDeletionErr := reconciler.Delete(ctx, providerSecret) + Expect(secretDeletionErr).NotTo(HaveOccurred()) + + By("Delete the tls secret") + secretDeletionErr = reconciler.Delete(ctx, tlsSecret) + Expect(secretDeletionErr).NotTo(HaveOccurred()) + + By("Delete the Openshift certificates config map") + configMapDeletionErr := reconciler.Delete(ctx, openshiftCertsConfigmap) + Expect(configMapDeletionErr).NotTo(HaveOccurred()) + }) + + It("should create a new LSC configmap when it does not exist", func() { + By("Reconcile the LSC configmap") + err := reconciler.reconcileLSCConfigMap(ctx, cr) + Expect(err).NotTo(HaveOccurred()) + + By("Get the configmap") + cm := &corev1.ConfigMap{} + err = k8sClient.Get(ctx, types.NamespacedName{Name: AppServerConfigCmName, Namespace: OLSNamespaceDefault}, cm) + Expect(err).NotTo(HaveOccurred()) + }) + }) + +}) diff --git a/internal/controller/olsconfig_controller.go b/internal/controller/olsconfig_controller.go index 764ec4566..79fead611 100644 --- a/internal/controller/olsconfig_controller.go +++ b/internal/controller/olsconfig_controller.go @@ -119,6 +119,19 @@ type OLSConfigReconcilerOptions struct { // +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=storage.k8s.io,resources=storageclasses,verbs=get;list;watch +// getAppServerReconcileFunction returns the appropriate reconcile function based on the presence of a configmap +func (r *OLSConfigReconciler) getAppServerReconcileFunction(ctx context.Context) (func(context.Context, *olsv1alpha1.OLSConfig) error, error) { + err := r.Get(ctx, client.ObjectKey{Name: LSCAppServerActivatorCmName, Namespace: r.Options.Namespace}, &corev1.ConfigMap{}) + if err != nil && apierrors.IsNotFound(err) { + return r.reconcileAppServer, nil + } + if err != nil { + return nil, fmt.Errorf("%s: %w", ErrGetLSCActivatorConfigmap, err) + } + return r.reconcileAppServerLSC, nil + +} + // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile func (r *OLSConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { @@ -171,6 +184,12 @@ func (r *OLSConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{RequeueAfter: 1 * time.Second}, err } + appServerReconcileFunc, err := r.getAppServerReconcileFunction(ctx) + if err != nil { + r.logger.Error(err, "Failed to get app server reconcile function") + return ctrl.Result{RequeueAfter: 1 * time.Second}, err + } + // Define reconciliation steps for all deployments with their associated status conditions reconcileSteps := []struct { name string @@ -180,7 +199,7 @@ func (r *OLSConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( }{ {"console UI", r.reconcileConsoleUI, typeConsolePluginReady, ConsoleUIDeploymentName}, {"postgres server", r.reconcilePostgresServer, typeCacheReady, PostgresDeploymentName}, - {"application server", r.reconcileAppServer, typeApiReady, OLSAppServerDeploymentName}, + {"application server", appServerReconcileFunc, typeApiReady, OLSAppServerDeploymentName}, } // Execute deployments reconcile diff --git a/test/e2e/lsc_backend_test.go b/test/e2e/lsc_backend_test.go new file mode 100644 index 000000000..b87776513 --- /dev/null +++ b/test/e2e/lsc_backend_test.go @@ -0,0 +1,62 @@ +package e2e + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + olsv1alpha1 "github.com/openshift/lightspeed-operator/api/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("Reconciliation From OLSConfig CR", Label("LSCBackend"), Ordered, func() { + var cr *olsv1alpha1.OLSConfig + var err error + var client *Client + + BeforeAll(func() { + client, err = GetClient(nil) + Expect(err).NotTo(HaveOccurred()) + By("Creating a OLSConfig CR") + cr, err = generateOLSConfig() + Expect(err).NotTo(HaveOccurred()) + err = client.Create(cr) + Expect(err).NotTo(HaveOccurred()) + }) + + AfterAll(func() { + client, err = GetClient(nil) + Expect(err).NotTo(HaveOccurred()) + err = mustGather("reconciliation_test") + Expect(err).NotTo(HaveOccurred()) + By("Deleting the OLSConfig CR") + Expect(cr).NotTo(BeNil()) + err = client.Delete(cr) + Expect(err).NotTo(HaveOccurred()) + + }) + + It("should setup LSC backend", func() { + By("Verify the LSC backend deployment running") + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: AppServerDeploymentName, + Namespace: OLSNameSpace, + }, + } + err = client.WaitForDeploymentRollout(deployment) + Expect(err).NotTo(HaveOccurred()) + + By("Verify the LSC backend service created") + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: AppServerServiceName, + Namespace: OLSNameSpace, + }, + } + err = client.WaitForServiceCreated(service) + Expect(err).NotTo(HaveOccurred()) + }) + + // todo: add test according to the LSC backend implementation details +})