From 1a8f841229543a696de79bc16dcba1bc20e0441d Mon Sep 17 00:00:00 2001 From: Jarek Cwiklik Date: Fri, 16 May 2025 11:17:13 -0400 Subject: [PATCH 1/3] initial implementation supporting kubernetes deployments Signed-off-by: Jarek Cwiklik --- .../api/v1alpha1/component_types.go | 5 +- .../api/v1alpha1/zz_generated.deepcopy.go | 12 + platform-operator/cmd/main.go | 14 + .../kagenti.operator.dev_components.yaml | 115 +++++++- .../config/crd/kustomization.yaml | 4 +- platform-operator/config/rbac/role.yaml | 55 +++- .../controller/component_controller.go | 205 ++++++++++++-- .../controller/platform_controller.go | 15 +- .../internal/deployer/deployer_factory.go | 6 +- .../internal/deployer/helm/helm.go | 4 + .../deployer/kubernetes/kubernetes.go | 259 +++++++++++++++++- .../internal/deployer/olm/olm.go | 5 + .../internal/deployer/types/deployer.go | 2 + 13 files changed, 654 insertions(+), 47 deletions(-) diff --git a/platform-operator/api/v1alpha1/component_types.go b/platform-operator/api/v1alpha1/component_types.go index 9e3bf09d..5e6c0e47 100644 --- a/platform-operator/api/v1alpha1/component_types.go +++ b/platform-operator/api/v1alpha1/component_types.go @@ -24,7 +24,6 @@ import ( type ComponentSpec struct { // Component Types - // +kubebuilder:validation:XValidation:rule="(has(self.agentComponent) ? 1 : 0) + (has(self.toolComponent) ? 1 : 0) + (has(self.infraComponent) ? 1 : 0) == 1",message="Exactly one component type must be specified" // Union pattern: only one of the following components should be specified. Agent *AgentComponent `json:"agentComponent,omitempty"` // MCP Servers, Utilities, etc @@ -216,6 +215,10 @@ type KubernetesSpec struct { // +optional Resources corev1.ResourceRequirements `json:"resources,omitempty"` + ContainerPorts []corev1.ContainerPort `json:"containerPorts,omitempty"` + + ServicePorts []corev1.ServicePort `json:"servicePorts,omitempty"` + // ServiceType is the type of service to create // +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer // +optional diff --git a/platform-operator/api/v1alpha1/zz_generated.deepcopy.go b/platform-operator/api/v1alpha1/zz_generated.deepcopy.go index e881d671..71f10881 100644 --- a/platform-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/platform-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -480,6 +480,18 @@ func (in *KubernetesSpec) DeepCopyInto(out *KubernetesSpec) { *out = *in out.Image = in.Image in.Resources.DeepCopyInto(&out.Resources) + if in.ContainerPorts != nil { + in, out := &in.ContainerPorts, &out.ContainerPorts + *out = make([]v1.ContainerPort, len(*in)) + copy(*out, *in) + } + if in.ServicePorts != nil { + in, out := &in.ServicePorts, &out.ServicePorts + *out = make([]v1.ServicePort, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesSpec. diff --git a/platform-operator/cmd/main.go b/platform-operator/cmd/main.go index 1b8eefb7..2ed2b1aa 100644 --- a/platform-operator/cmd/main.go +++ b/platform-operator/cmd/main.go @@ -39,6 +39,10 @@ import ( kagentioperatordevv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" "github.com/kagenti/operator/platform/internal/controller" + "github.com/kagenti/operator/platform/internal/deployer" + "github.com/kagenti/operator/platform/internal/deployer/helm" + "github.com/kagenti/operator/platform/internal/deployer/kubernetes" + "github.com/kagenti/operator/platform/internal/deployer/olm" // +kubebuilder:scaffold:imports ) @@ -205,6 +209,15 @@ func main() { if err = (&controller.ComponentReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + Log: ctrl.Log.WithName("controllers").WithName("Component"), + DeployerFactory: &deployer.DeployerFactory{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: ctrl.Log.WithName("deployers").WithName("Deployer"), + KubeDeployer: kubernetes.NewKubernetesDeployer(mgr.GetClient(), ctrl.Log.WithName("deployers").WithName("KubernetesDeployer"), mgr.GetScheme()), + HelmDeployer: helm.NewHelmDeployer(mgr.GetClient(), ctrl.Log.WithName("deployers").WithName("HelmDeployer"), mgr.GetScheme()), + OLMDeployer: olm.NewOLMDeployer(mgr.GetClient(), ctrl.Log.WithName("deployers").WithName("OLMDeployer"), mgr.GetScheme()), + }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Component") os.Exit(1) @@ -212,6 +225,7 @@ func main() { if err = (&controller.PlatformReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + Log: ctrl.Log.WithName("platforms").WithName("Platform"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Platform") os.Exit(1) diff --git a/platform-operator/config/crd/bases/kagenti.operator.dev_components.yaml b/platform-operator/config/crd/bases/kagenti.operator.dev_components.yaml index 73c95f70..24fd1df5 100644 --- a/platform-operator/config/crd/bases/kagenti.operator.dev_components.yaml +++ b/platform-operator/config/crd/bases/kagenti.operator.dev_components.yaml @@ -122,10 +122,6 @@ spec: - sourceRevision type: object type: object - x-kubernetes-validations: - - message: Exactly one component type must be specified - rule: '(has(self.agentComponent) ? 1 : 0) + (has(self.toolComponent) - ? 1 : 0) + (has(self.infraComponent) ? 1 : 0) == 1' dependencies: description: Dependencies defines other components this agent depends on @@ -306,6 +302,45 @@ spec: kubernetesSpec: description: KubernetesSpec defines Kubernetes manifest deployment properties: + containerPorts: + items: + description: ContainerPort represents a network port in + a single container. + properties: + containerPort: + description: |- + Number of port to expose on the pod's IP address. + This must be a valid port number, 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external port + to. + type: string + hostPort: + description: |- + Number of port to expose on the host. + If specified, this must be a valid port number, 0 < x < 65536. + If HostNetwork is specified, this must match ContainerPort. + Most containers do not need this. + format: int32 + type: integer + name: + description: |- + If specified, this must be an IANA_SVC_NAME and unique within the pod. Each + named port in a pod must have a unique name. Name for the port that can be + referred to by services. + type: string + protocol: + default: TCP + description: |- + Protocol for port. Must be UDP, TCP, or SCTP. + Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array imageSpec: properties: image: @@ -387,6 +422,78 @@ spec: More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ type: object type: object + servicePorts: + items: + description: ServicePort contains information on service's + port. + properties: + appProtocol: + description: |- + The application protocol for this port. + This is used as a hint for implementations to offer richer behavior for protocols that they understand. + This field follows standard Kubernetes label syntax. + Valid values are either: + + * Un-prefixed protocol names - reserved for IANA standard service names (as per + RFC-6335 and https://www.iana.org/assignments/service-names). + + * Kubernetes-defined prefixed names: + * 'kubernetes.io/h2c' - HTTP/2 prior knowledge over cleartext as described in https://www.rfc-editor.org/rfc/rfc9113.html#name-starting-http-2-with-prior- + * 'kubernetes.io/ws' - WebSocket over cleartext as described in https://www.rfc-editor.org/rfc/rfc6455 + * 'kubernetes.io/wss' - WebSocket over TLS as described in https://www.rfc-editor.org/rfc/rfc6455 + + * Other protocols should use implementation-defined prefixed names such as + mycompany.com/my-custom-protocol. + type: string + name: + description: |- + The name of this port within the service. This must be a DNS_LABEL. + All ports within a ServiceSpec must have unique names. When considering + the endpoints for a Service, this must match the 'name' field in the + EndpointPort. + Optional if only one ServicePort is defined on this service. + type: string + nodePort: + description: |- + The port on each node on which this service is exposed when type is + NodePort or LoadBalancer. Usually assigned by the system. If a value is + specified, in-range, and not in use it will be used, otherwise the + operation will fail. If not specified, a port will be allocated if this + Service requires one. If this field is specified when creating a + Service which does not need it, creation will fail. This field will be + wiped when updating a Service to no longer need it (e.g. changing type + from NodePort to ClusterIP). + More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP + description: |- + The IP protocol for this port. Supports "TCP", "UDP", and "SCTP". + Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: |- + Number or name of the port to access on the pods targeted by the service. + Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named port in the + target Pod's container ports. If this is not specified, the value + of the 'port' field is used (an identity map). + This field is ignored for services with clusterIP=None, and should be + omitted or set equal to the 'port' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array serviceType: description: ServiceType is the type of service to create enum: diff --git a/platform-operator/config/crd/kustomization.yaml b/platform-operator/config/crd/kustomization.yaml index c8c8488a..525ae084 100644 --- a/platform-operator/config/crd/kustomization.yaml +++ b/platform-operator/config/crd/kustomization.yaml @@ -2,8 +2,8 @@ # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: -- bases/kagenti.operator.dev.kagenti.operator.dev_components.yaml -- bases/kagenti.operator.dev.kagenti.operator.dev_platforms.yaml +- bases/kagenti.operator.dev_components.yaml +- bases/kagenti.operator.dev_platforms.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/platform-operator/config/rbac/role.yaml b/platform-operator/config/rbac/role.yaml index 2a95d93a..74810c05 100644 --- a/platform-operator/config/rbac/role.yaml +++ b/platform-operator/config/rbac/role.yaml @@ -5,9 +5,60 @@ metadata: name: manager-role rules: - apiGroups: - - kagenti.operator.dev.kagenti.operator.dev + - "" + resources: + - configmaps + - secrets + - services + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - kagenti.operator.dev resources: - components + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - kagenti.operator.dev + resources: + - components/finalizers + verbs: + - update +- apiGroups: + - kagenti.operator.dev + resources: + - components/status + verbs: + - get + - patch + - update +- apiGroups: + - kagenti.operator.dev.kagenti.operator.dev + resources: - platforms verbs: - create @@ -20,14 +71,12 @@ rules: - apiGroups: - kagenti.operator.dev.kagenti.operator.dev resources: - - components/finalizers - platforms/finalizers verbs: - update - apiGroups: - kagenti.operator.dev.kagenti.operator.dev resources: - - components/status - platforms/status verbs: - get diff --git a/platform-operator/internal/controller/component_controller.go b/platform-operator/internal/controller/component_controller.go index d2b38e94..ae14be14 100644 --- a/platform-operator/internal/controller/component_controller.go +++ b/platform-operator/internal/controller/component_controller.go @@ -18,38 +18,207 @@ package controller import ( "context" + "fmt" + "time" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - kagentioperatordevv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" + //"sigs.k8s.io/controller-runtime/pkg/log" + "github.com/go-logr/logr" + platformv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" + "github.com/kagenti/operator/platform/internal/deployer" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ComponentReconciler reconciles a Component object type ComponentReconciler struct { - client.Client + Client client.Client Scheme *runtime.Scheme + Log logr.Logger + + DeployerFactory *deployer.DeployerFactory } -// +kubebuilder:rbac:groups=kagenti.operator.dev.kagenti.operator.dev,resources=components,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=kagenti.operator.dev.kagenti.operator.dev,resources=components/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=kagenti.operator.dev.kagenti.operator.dev,resources=components/finalizers,verbs=update +const componentFinalizer = "kagenti.operator.dev/finalizer" + +// +kubebuilder:rbac:groups=kagenti.operator.dev,resources=components,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kagenti.operator.dev,resources=components/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kagenti.operator.dev,resources=components/finalizers,verbs=update +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=services;configmaps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the Component object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + logger := r.Log.WithValues("controller", req.Name, req.Namespace) + logger.Info("Reconciling component") + + component := &platformv1alpha1.Component{} + if err := r.Client.Get(ctx, req.NamespacedName, component); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if !component.ObjectMeta.DeletionTimestamp.IsZero() { + return r.deleteComponent(ctx, component) + } + + if !controllerutil.ContainsFinalizer(component, componentFinalizer) { + controllerutil.AddFinalizer(component, componentFinalizer) + if err := r.Client.Update(ctx, component); err != nil { + logger.Error(err, "Unable to add finalizer to Component") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + doDeploy, err := r.isDeploymentNeeded(component) + if err != nil { + logger.Error(err, "Failed to determine if deployment is needed") + return ctrl.Result{}, err + } + if doDeploy { + logger.Info("Starting component deployment") + component.Status.DeploymentStatus.Phase = "Deploying" + component.Status.DeploymentStatus.DeploymentMessage = "Deployment in progress" + if err := r.Client.Status().Update(ctx, component); err != nil { + return ctrl.Result{}, err + } + + deployer, err := r.DeployerFactory.GetDeployer(component) + if err != nil { + component.Status.DeploymentStatus.Phase = "Failed" + component.Status.DeploymentStatus.DeploymentMessage = "Invalid deployer for the component" + err = r.Client.Status().Update(ctx, component) + if err != nil { + return ctrl.Result{}, err + } + } + err = deployer.Deploy(ctx, component) + if err != nil { + component.Status.DeploymentStatus.Phase = "Failed" + component.Status.DeploymentStatus.DeploymentMessage = "Failed to deploy the component" + err = r.Client.Status().Update(ctx, component) + if err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{RequeueAfter: time.Second * 20}, nil + } + if component.Status.DeploymentStatus != nil && component.Status.DeploymentStatus.Phase == "Deploying" { + if err := r.checkDeploymentStatus(ctx, component); err != nil { + logger.Error(err, "Failed to check deployment status") + return ctrl.Result{}, nil + } + return ctrl.Result{RequeueAfter: time.Second * 20}, nil + } + if r.updateComponentStatus(ctx, component); err != nil { + logger.Error(err, "Failed to update component status") + return ctrl.Result{}, nil + } + logger.Info("Component reconciliation competed successfully") + return ctrl.Result{RequeueAfter: time.Second * 50}, nil +} +func (r *ComponentReconciler) updateComponentStatus(ctx context.Context, component *platformv1alpha1.Component) error { + ready := true + reason := "Ready" + message := "Component is ready" + + if r.hasBuildSpec(component) && component.Status.BuildStatus != nil { + if component.Status.BuildStatus.Phase != "Succeeded" { + ready = false + reason = fmt.Sprintf("BuildNotReady:%s", component.Status.BuildStatus.Phase) + message = fmt.Sprintf("Build is not ready: %s", component.Status.BuildStatus.Message) + } + } + if component.Status.DeploymentStatus != nil && component.Status.DeploymentStatus.Phase != "Ready" { + ready = false + reason = fmt.Sprintf("DeploymentNotReady:%s", component.Status.DeploymentStatus.Phase) + message = fmt.Sprintf("Deployment is not ready: %s", component.Status.DeploymentStatus.DeploymentMessage) + } + + readyCondition := metav1.Condition{ + Type: "Ready", + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } + + if ready { + readyCondition.Status = metav1.ConditionTrue + } else { + readyCondition.Status = metav1.ConditionFalse + } + + r.updateComponentCondition(component, readyCondition) - // TODO(user): your logic here + now := metav1.Now() + component.Status.LastTransitionTime = &now + return r.Client.Status().Update(ctx, component) +} + +func (r *ComponentReconciler) updateComponentCondition(component *platformv1alpha1.Component, condition metav1.Condition) { + for i, cond := range component.Status.Conditions { + if cond.Type == condition.Type { + if cond.Status == condition.Status && + cond.Reason == condition.Reason && + cond.Message == cond.Message { + return + } + if cond.Status != condition.Status { + condition.LastTransitionTime = metav1.Now() + } else { + condition.LastTransitionTime = cond.LastTransitionTime + } + component.Status.Conditions[i] = condition + return + } + } + component.Status.Conditions = append(component.Status.Conditions, condition) +} +func (r *ComponentReconciler) checkDeploymentStatus(ctx context.Context, component *platformv1alpha1.Component) error { + logger := r.Log.WithValues("component", component.Name, component.Namespace) + logger.Info("Checking component status") + + deployer, err := r.DeployerFactory.GetDeployer(component) + if err != nil { + component.Status.DeploymentStatus.Phase = "Failed" + component.Status.DeploymentStatus.DeploymentMessage = "Invalid deployer for the component" + err = r.Client.Status().Update(ctx, component) + if err != nil { + return err + } + } + + ready, message, err := deployer.CheckComponentStatus(ctx, component) + component.Status.DeploymentStatus.DeploymentMessage = message + if ready { + component.Status.DeploymentStatus.Phase = "Ready" + } else { + component.Status.DeploymentStatus.Phase = "Deploying" + } + return r.Client.Status().Update(ctx, component) +} + +func (r *ComponentReconciler) isDeploymentNeeded(component *platformv1alpha1.Component) (bool, error) { + if component.Status.DeploymentStatus == nil || component.Status.DeploymentStatus.Phase == "Pending" { + return true, nil + } + if component.Status.DeploymentStatus.Phase == "Failed" { + return true, nil + } + if r.hasBuildSpec(component) && + (component.Status.BuildStatus == nil || component.Status.BuildStatus.Phase != "Succeeded") { + return false, nil + } + + return false, nil +} +func (r *ComponentReconciler) hasBuildSpec(component *platformv1alpha1.Component) bool { + return component.Spec.Agent != nil && component.Spec.Agent.Build != nil || + component.Spec.Tool != nil && component.Spec.Tool.Build != nil +} +func (r *ComponentReconciler) deleteComponent(ctx context.Context, component *platformv1alpha1.Component) (ctrl.Result, error) { return ctrl.Result{}, nil } @@ -57,7 +226,7 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( // SetupWithManager sets up the controller with the Manager. func (r *ComponentReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&kagentioperatordevv1alpha1.Component{}). + For(&platformv1alpha1.Component{}). Named("component"). Complete(r) } diff --git a/platform-operator/internal/controller/platform_controller.go b/platform-operator/internal/controller/platform_controller.go index 27cd91a2..d7a97fef 100644 --- a/platform-operator/internal/controller/platform_controller.go +++ b/platform-operator/internal/controller/platform_controller.go @@ -24,6 +24,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/go-logr/logr" kagentioperatordevv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" ) @@ -31,21 +32,9 @@ import ( type PlatformReconciler struct { client.Client Scheme *runtime.Scheme + Log logr.Logger } -// +kubebuilder:rbac:groups=kagenti.operator.dev.kagenti.operator.dev,resources=platforms,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=kagenti.operator.dev.kagenti.operator.dev,resources=platforms/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=kagenti.operator.dev.kagenti.operator.dev,resources=platforms/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the Platform object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile func (r *PlatformReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) diff --git a/platform-operator/internal/deployer/deployer_factory.go b/platform-operator/internal/deployer/deployer_factory.go index 4ac7f9fe..a011268a 100644 --- a/platform-operator/internal/deployer/deployer_factory.go +++ b/platform-operator/internal/deployer/deployer_factory.go @@ -1,7 +1,7 @@ package deployer import ( - "context" + //"context" "fmt" "github.com/go-logr/logr" @@ -37,7 +37,7 @@ func NewDeployerFactory(client client.Client, log logr.Logger, scheme *runtime.S return factory } -func (d *DeployerFactory) getDeployer(component *platformv1alpha1.Component) (types.ComponentDeployer, error) { +func (d *DeployerFactory) GetDeployer(component *platformv1alpha1.Component) (types.ComponentDeployer, error) { if component == nil { return nil, fmt.Errorf("unable to get Deployer for undefined (nil) component") } @@ -56,6 +56,7 @@ func (d *DeployerFactory) getDeployer(component *platformv1alpha1.Component) (ty return nil, fmt.Errorf("No valid deployer found for component %s/%s", component.Namespace, component.Name) } +/* // DeployComponent is a convenience method to deploy a component using the appropriate deployer func (f *DeployerFactory) DeployComponent(ctx context.Context, component *platformv1alpha1.Component) error { deployer, err := f.getDeployer(component) @@ -89,3 +90,4 @@ func (f *DeployerFactory) GetComponentStatus(ctx context.Context, component *pla } return deployer.GetStatus(ctx, component) } +*/ diff --git a/platform-operator/internal/deployer/helm/helm.go b/platform-operator/internal/deployer/helm/helm.go index 89cac758..59253e7b 100644 --- a/platform-operator/internal/deployer/helm/helm.go +++ b/platform-operator/internal/deployer/helm/helm.go @@ -48,3 +48,7 @@ func (b *HelmDeployer) GetStatus(ctx context.Context, component *platformv1alpha return platformv1alpha1.ComponentDeploymentStatus{}, nil } +func (d *HelmDeployer) CheckComponentStatus(ctx context.Context, component *platformv1alpha1.Component) (bool, string, error) { + + return false, "Not implemented", nil +} diff --git a/platform-operator/internal/deployer/kubernetes/kubernetes.go b/platform-operator/internal/deployer/kubernetes/kubernetes.go index 1ab2acec..2b7be06a 100644 --- a/platform-operator/internal/deployer/kubernetes/kubernetes.go +++ b/platform-operator/internal/deployer/kubernetes/kubernetes.go @@ -2,12 +2,20 @@ package kubernetes import ( "context" + "fmt" + "strconv" "github.com/go-logr/logr" platformv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" "github.com/kagenti/operator/platform/internal/deployer/types" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) var _ types.ComponentDeployer = (*KubernetesDeployer)(nil) @@ -25,26 +33,269 @@ func NewKubernetesDeployer(client client.Client, log logr.Logger, scheme *runtim Scheme: scheme, } } -func (b *KubernetesDeployer) Deploy(ctx context.Context, component *platformv1alpha1.Component) error { +func (d *KubernetesDeployer) Deploy(ctx context.Context, component *platformv1alpha1.Component) error { + + logger := d.Log.WithValues("deployer", component.Name, component.Namespace) + logger.Info("Deploying component with Kubernetes resources") + + namespace := component.Namespace + if component.Spec.Deployer.Namespace != "" { + namespace = component.Spec.Deployer.Namespace + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, + } + if err := d.Client.Create(ctx, ns); err != nil { + if !errors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create namespace: %w", err) + } + } + if err := d.createDeployment(ctx, component, namespace); err != nil { + return fmt.Errorf("failed to create deployment: %w", err) + } + + if err := d.createService(ctx, component, namespace); err != nil { + return fmt.Errorf("failed to create service: %w", err) + } return nil } // Update existing component -func (b *KubernetesDeployer) Update(ctx context.Context, component *platformv1alpha1.Component) error { +func (d *KubernetesDeployer) Update(ctx context.Context, component *platformv1alpha1.Component) error { return nil } // Delete existing component -func (b *KubernetesDeployer) Delete(ctx context.Context, component *platformv1alpha1.Component) error { +func (d *KubernetesDeployer) Delete(ctx context.Context, component *platformv1alpha1.Component) error { return nil } // Return status of the component -func (b *KubernetesDeployer) GetStatus(ctx context.Context, component *platformv1alpha1.Component) (platformv1alpha1.ComponentDeploymentStatus, error) { +func (d *KubernetesDeployer) GetStatus(ctx context.Context, component *platformv1alpha1.Component) (platformv1alpha1.ComponentDeploymentStatus, error) { return platformv1alpha1.ComponentDeploymentStatus{}, nil } +func (d *KubernetesDeployer) createDeployment(ctx context.Context, component *platformv1alpha1.Component, namespace string) error { + + kubeSpec := component.Spec.Deployer.Kubernetes + + if kubeSpec == nil { + return fmt.Errorf("failed to create deployment - missing expected Spec.Deployer.Kubernetes in the CR") + } + containerPorts := []corev1.ContainerPort{} + + if component.Spec.Deployer.Kubernetes.ContainerPorts != nil { + for _, port := range component.Spec.Deployer.Kubernetes.ContainerPorts { + containerPorts = append(containerPorts, corev1.ContainerPort{ + Name: port.Name, + ContainerPort: port.ContainerPort, + Protocol: port.Protocol, + }) + } + } + labels := map[string]string{ + "app.kubernetes.io/name": component.Name, + "app.kubernetes.io/part-of": "platform-operator", + "app.kuberbetes.io/managed-by": "platform-operator", + "app.kubernetes.io/component": getComponentType(component), + } + for k, v := range component.Labels { + if _, exists := labels[k]; !exists { + labels[k] = v + } + } + image := fmt.Sprintf("%s/%s:%s", + kubeSpec.Image.ImageRegistry, + kubeSpec.Image.Image, + kubeSpec.Image.ImageTag, + ) + + deployment := &appsv1.Deployment{ + + ObjectMeta: metav1.ObjectMeta{ + Name: component.Name, + Namespace: component.Namespace, + Labels: labels, + }, + + Spec: appsv1.DeploymentSpec{ + Replicas: getReplicaCount(component), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app.kubernetes.io/name": component.Name, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: component.Name, + Image: image, + ImagePullPolicy: corev1.PullPolicy(kubeSpec.Image.ImagePullPolicy), + Resources: component.Spec.Deployer.Kubernetes.Resources, + Env: component.Spec.Deployer.Env, + Ports: containerPorts, + }, + }, + }, + }, + }, + } + + if component.Annotations != nil { + deployment.ObjectMeta.Annotations = component.Annotations + } + + if err := controllerutil.SetControllerReference(component, deployment, d.Client.Scheme()); err != nil { + return err + } + + existingDeployment := &appsv1.Deployment{} + err := d.Client.Get(ctx, k8stypes.NamespacedName{Name: deployment.Name, Namespace: deployment.Namespace}, existingDeployment) + if err != nil { + if errors.IsNotFound(err) { + return d.Client.Create(ctx, deployment) + } + return err + } + return nil + +} +func (d *KubernetesDeployer) createService(ctx context.Context, component *platformv1alpha1.Component, namespace string) error { + + kubeSpec := component.Spec.Deployer.Kubernetes + + if kubeSpec == nil { + return fmt.Errorf("failed to create deployment - missing expected Spec.Deployer.Kubernetes in the CR") + } + labels := map[string]string{ + "app.kubernetes.io/name": component.Name, + "app.kubernetes.io/part-of": "platform-operator", + "app.kuberbetes.io/managed-by": "platform-operator", + "app.kubernetes.io/component": getComponentType(component), + } + for k, v := range component.Labels { + if _, exists := labels[k]; !exists { + labels[k] = v + } + } + servicePorts := []corev1.ServicePort{} + + if component.Spec.Deployer.Kubernetes.ServicePorts != nil { + for _, port := range component.Spec.Deployer.Kubernetes.ServicePorts { + servicePorts = append(servicePorts, corev1.ServicePort{ + Name: port.Name, + Port: port.Port, + TargetPort: port.TargetPort, + Protocol: corev1.ProtocolTCP, + }) + } + } + service := &corev1.Service{ + + ObjectMeta: metav1.ObjectMeta{ + Name: component.Name, + Namespace: component.Namespace, + Labels: labels, + }, + + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "app.kubernetes.io/name": component.Name, + }, + Ports: servicePorts, + Type: corev1.ServiceType(kubeSpec.ServiceType), + }, + } + if component.Annotations != nil { + service.ObjectMeta.Annotations = component.Annotations + } + + if err := controllerutil.SetControllerReference(component, service, d.Client.Scheme()); err != nil { + return err + } + + existigService := &corev1.Service{} + err := d.Client.Get(ctx, k8stypes.NamespacedName{Name: service.Name, Namespace: service.Namespace}, existigService) + if err != nil { + if errors.IsNotFound(err) { + return d.Client.Create(ctx, existigService) + } + return err + } + return nil +} +func getComponentType(component *platformv1alpha1.Component) string { + if component.Spec.Agent != nil { + return "agent" + } + if component.Spec.Tool != nil { + return "tool" + } + if component.Spec.Infra != nil { + return "infra" + } + return "unknown" +} +func getReplicaCount(component *platformv1alpha1.Component) *int32 { + var count int32 = 1 + + if component.Annotations != nil { + if replicaStr, ok := component.Annotations["platform.operator.io/replicates"]; ok { + if replicas, err := strconv.ParseInt(replicaStr, 10, 32); err == nil { + count = int32(replicas) + } + } + } + return &count +} +func (d *KubernetesDeployer) CheckComponentStatus(ctx context.Context, component *platformv1alpha1.Component) (bool, string, error) { + + d.Log.WithValues("component", component.Name, "namespace", component.Namespace) + + namespace := component.Namespace + if component.Spec.Deployer.Namespace != "" { + namespace = component.Spec.Deployer.Namespace + } + deployment := &appsv1.Deployment{} + err := d.Client.Get(ctx, k8stypes.NamespacedName{Name: component.Name, Namespace: namespace}, deployment) + if err != nil { + if errors.IsNotFound(err) { + return false, "Deployment not found", nil + } + d.Log.Error(err, "Failed to get deployment object") + return false, fmt.Sprintf("Error checking deployment: %v", err), err + } + + if deployment.Status.ReadyReplicas < 1 { + message := fmt.Sprintf("Deployment not ready: %d/%d replicas available", + deployment.Status.ReadyReplicas, *deployment.Spec.Replicas) + for _, condition := range deployment.Status.Conditions { + message = fmt.Sprintf("%s, Reason: %s, Message: %s", message, condition.Reason, condition.Message) + break + } + return false, message, nil + } + kubeSpec := component.Spec.Deployer.Kubernetes + if kubeSpec.ServiceType != "" { + service := &corev1.Service{} + err := d.Client.Get(ctx, k8stypes.NamespacedName{Name: component.Name, Namespace: namespace}, service) + if err != nil { + if errors.IsNotFound(err) { + return false, "Service not found", nil + } + d.Log.Error(err, "Failed to get service") + return false, fmt.Sprintf("Error checking service: %v", err), err + } + } + return true, "Component is ready", nil +} diff --git a/platform-operator/internal/deployer/olm/olm.go b/platform-operator/internal/deployer/olm/olm.go index 3052bfe4..ffc6a110 100644 --- a/platform-operator/internal/deployer/olm/olm.go +++ b/platform-operator/internal/deployer/olm/olm.go @@ -48,3 +48,8 @@ func (b *OLMDeployer) GetStatus(ctx context.Context, component *platformv1alpha1 return platformv1alpha1.ComponentDeploymentStatus{}, nil } + +func (d *OLMDeployer) CheckComponentStatus(ctx context.Context, component *platformv1alpha1.Component) (bool, string, error) { + + return false, "Not implemented", nil +} diff --git a/platform-operator/internal/deployer/types/deployer.go b/platform-operator/internal/deployer/types/deployer.go index 099feeba..a6b16851 100644 --- a/platform-operator/internal/deployer/types/deployer.go +++ b/platform-operator/internal/deployer/types/deployer.go @@ -15,4 +15,6 @@ type ComponentDeployer interface { Delete(ctx context.Context, component *platformv1alpha1.Component) error // Return status of the component GetStatus(ctx context.Context, component *platformv1alpha1.Component) (platformv1alpha1.ComponentDeploymentStatus, error) + // Returns if component pods are ready or not + CheckComponentStatus(ctx context.Context, component *platformv1alpha1.Component) (bool, string, error) } From 359b029209b2eacf071d0e9d570e8e4bf2e3bfdf Mon Sep 17 00:00:00 2001 From: Jarek Cwiklik Date: Fri, 16 May 2025 16:06:03 -0400 Subject: [PATCH 2/3] initial implementation for platform controller Signed-off-by: Jarek Cwiklik --- platform-operator/cmd/main.go | 8 +- platform-operator/config/rbac/role.yaml | 26 -- .../controller/component_controller.go | 74 ++++- .../controller/platform_controller.go | 303 +++++++++++++++++- .../deployer/kubernetes/kubernetes.go | 42 ++- 5 files changed, 407 insertions(+), 46 deletions(-) diff --git a/platform-operator/cmd/main.go b/platform-operator/cmd/main.go index 2ed2b1aa..04bb7e84 100644 --- a/platform-operator/cmd/main.go +++ b/platform-operator/cmd/main.go @@ -38,6 +38,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" kagentioperatordevv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" + "github.com/kagenti/operator/platform/internal/builder/tekton" "github.com/kagenti/operator/platform/internal/controller" "github.com/kagenti/operator/platform/internal/deployer" "github.com/kagenti/operator/platform/internal/deployer/helm" @@ -207,9 +208,10 @@ func main() { } if err = (&controller.ComponentReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: ctrl.Log.WithName("controllers").WithName("Component"), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: ctrl.Log.WithName("controllers").WithName("Component"), + Builder: tekton.NewTektonBuilder(mgr.GetClient(), mgr.GetLogger(), mgr.GetScheme()), DeployerFactory: &deployer.DeployerFactory{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/platform-operator/config/rbac/role.yaml b/platform-operator/config/rbac/role.yaml index 74810c05..2df33d60 100644 --- a/platform-operator/config/rbac/role.yaml +++ b/platform-operator/config/rbac/role.yaml @@ -56,29 +56,3 @@ rules: - get - patch - update -- apiGroups: - - kagenti.operator.dev.kagenti.operator.dev - resources: - - platforms - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - kagenti.operator.dev.kagenti.operator.dev - resources: - - platforms/finalizers - verbs: - - update -- apiGroups: - - kagenti.operator.dev.kagenti.operator.dev - resources: - - platforms/status - verbs: - - get - - patch - - update diff --git a/platform-operator/internal/controller/component_controller.go b/platform-operator/internal/controller/component_controller.go index ae14be14..4a209c6a 100644 --- a/platform-operator/internal/controller/component_controller.go +++ b/platform-operator/internal/controller/component_controller.go @@ -21,24 +21,26 @@ import ( "fmt" "time" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - //"sigs.k8s.io/controller-runtime/pkg/log" "github.com/go-logr/logr" platformv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" + "github.com/kagenti/operator/platform/internal/builder" "github.com/kagenti/operator/platform/internal/deployer" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ComponentReconciler reconciles a Component object type ComponentReconciler struct { - Client client.Client - Scheme *runtime.Scheme - Log logr.Logger - + Client client.Client + Scheme *runtime.Scheme + Log logr.Logger + Builder builder.Builder DeployerFactory *deployer.DeployerFactory } @@ -64,6 +66,13 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return r.deleteComponent(ctx, component) } + if len(component.Status.Conditions) == 0 { + if err := r.initializeComponentStatus(ctx, component); err != nil { + logger.Error(err, "Failed to initialize component status") + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } if !controllerutil.ContainsFinalizer(component, componentFinalizer) { controllerutil.AddFinalizer(component, componentFinalizer) if err := r.Client.Update(ctx, component); err != nil { @@ -119,6 +128,34 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( logger.Info("Component reconciliation competed successfully") return ctrl.Result{RequeueAfter: time.Second * 50}, nil } +func (r *ComponentReconciler) initializeComponentStatus(ctx context.Context, component *platformv1alpha1.Component) error { + now := metav1.Now() + component.Status.Conditions = []metav1.Condition{ + { + Type: "Ready", + Status: metav1.ConditionFalse, + LastTransitionTime: now, + Reason: "Initializing", + Message: "Component is being initialized", + }, + } + component.Status.LastTransitionTime = &now + + if r.hasBuildSpec(component) { + component.Status.BuildStatus = &platformv1alpha1.BuildStatus{ + Phase: "Pending", + Message: "Build is pending", + } + } + + component.Status.DeploymentStatus = &platformv1alpha1.ComponentDeploymentStatus{ + Phase: "Pending", + DeploymentMessage: "Deployment is pending", + } + + return r.Client.Status().Update(ctx, component) +} + func (r *ComponentReconciler) updateComponentStatus(ctx context.Context, component *platformv1alpha1.Component) error { ready := true reason := "Ready" @@ -220,6 +257,27 @@ func (r *ComponentReconciler) hasBuildSpec(component *platformv1alpha1.Component } func (r *ComponentReconciler) deleteComponent(ctx context.Context, component *platformv1alpha1.Component) (ctrl.Result, error) { + if component.Status.BuildStatus != nil && component.Status.BuildStatus.Phase == "Building" { + r.Log.Info("Cancelling in-progress build") + if err := r.Builder.Cancel(ctx, component); err != nil { + r.Log.Error(err, "Error while cancelling component build") + } + } + if component.Status.DeploymentStatus != nil { + componentDeployer, err := r.DeployerFactory.GetDeployer(component) + if err != nil { + r.Log.Error(err, "Unable to determine deployer for component") + return ctrl.Result{}, err + } + if err := componentDeployer.Delete(ctx, component); err != nil { + r.Log.Error(err, "Failed to delete component") + return ctrl.Result{}, err + } + } + controllerutil.RemoveFinalizer(component, componentFinalizer) + if err := r.Client.Update(ctx, component); err != nil { + return ctrl.Result{}, err + } return ctrl.Result{}, nil } @@ -227,6 +285,8 @@ func (r *ComponentReconciler) deleteComponent(ctx context.Context, component *pl func (r *ComponentReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&platformv1alpha1.Component{}). - Named("component"). + Owns(&appsv1.Deployment{}). + Owns(&corev1.Service{}). + Owns(&corev1.ConfigMap{}). Complete(r) } diff --git a/platform-operator/internal/controller/platform_controller.go b/platform-operator/internal/controller/platform_controller.go index d7a97fef..19cfbc6e 100644 --- a/platform-operator/internal/controller/platform_controller.go +++ b/platform-operator/internal/controller/platform_controller.go @@ -18,14 +18,18 @@ package controller import ( "context" + "fmt" + "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/go-logr/logr" - kagentioperatordevv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" + platformv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" ) // PlatformReconciler reconciles a Platform object @@ -35,18 +39,305 @@ type PlatformReconciler struct { Log logr.Logger } +const platformFinalizer = "kagenti.operator.dev/finalizer" + func (r *PlatformReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + logger := r.Log.WithValues("platform", req.Name, req.Namespace) + logger.Info("Reconciling agentic platform") + + platform := &platformv1alpha1.Platform{} + if err := r.Client.Get(ctx, req.NamespacedName, platform); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if platform.Status.Phase == "" { + if err := r.initializePlatformStatus(ctx, platform); err != nil { + logger.Error(err, "Failed to initialize platform status") + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + + if !controllerutil.ContainsFinalizer(platform, platformFinalizer) { + controllerutil.AddFinalizer(platform, platformFinalizer) + if err := r.Client.Update(ctx, platform); err != nil { + logger.Error(err, "Unable to add finalizer to Platform") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + if !platform.ObjectMeta.DeletionTimestamp.IsZero() { + return r.deletePlatform(ctx, platform) + } + + if err := r.reconcileComponents(ctx, platform, platform.Spec.Infrastructure, "infrastructure"); err != nil { + return ctrl.Result{}, err + } + if err := r.reconcileComponents(ctx, platform, platform.Spec.Tools, "tools"); err != nil { + return ctrl.Result{}, err + } + if err := r.reconcileComponents(ctx, platform, platform.Spec.Agents, "agents"); err != nil { + return ctrl.Result{}, err + } + if err := r.updatePlatformStatus(ctx, platform); err != nil { + logger.Error(err, "Faled to update platform status") + return ctrl.Result{}, err + } + return ctrl.Result{RequeueAfter: time.Second * 30}, nil +} +func (r *PlatformReconciler) updatePlatformStatus(ctx context.Context, platform *platformv1alpha1.Platform) error { + + allReady := true + failedComponents := []string{} - // TODO(user): your logic here + checkComponentList := func(components []platformv1alpha1.DeploymentStatus) { + for _, comp := range components { + if comp.Status != "Ready" { + allReady = false + if comp.Status == "Failed" { + failedComponents = append(failedComponents, comp.Name) + } + } + } + } + checkComponentList(platform.Status.Components.Infrastructure) + checkComponentList(platform.Status.Components.Tools) + checkComponentList(platform.Status.Components.Agents) + + oldPhase := platform.Status.Phase + if allReady { + platform.Status.Phase = "Ready" + } else if len(failedComponents) > 0 { + platform.Status.Phase = "Failed" + } else { + platform.Status.Phase = "Deploying" + } + readyCondition := metav1.Condition{ + Type: "Ready", + LastTransitionTime: metav1.Now(), + } + + if allReady { + readyCondition.Status = metav1.ConditionTrue + readyCondition.Reason = "AllComponentsReady" + readyCondition.Message = "All platform components are ready" + } else if len(failedComponents) > 0 { + readyCondition.Status = metav1.ConditionFalse + readyCondition.Reason = "Components Failed" + readyCondition.Message = fmt.Sprintf("Some components failed: %v", failedComponents) + } else { + readyCondition.Status = metav1.ConditionFalse + readyCondition.Reason = "ComponentsDeploying" + readyCondition.Message = "Components are still being deployed" + } + r.updateCondition(&platform.Status.Conditions, readyCondition) + if oldPhase != platform.Status.Phase { + r.Log.Info("Updating platform status", + "platform", platform.Name, + "oldPhase", oldPhase, + "newPhase", platform.Status.Phase) + return r.Client.Status().Update(ctx, platform) + } + return nil +} +func (r *PlatformReconciler) updateCondition(conditions *[]metav1.Condition, condition metav1.Condition) { + for i, cond := range *conditions { + if cond.Type == condition.Type { + if cond.Status == condition.Status && + cond.Reason == condition.Reason && + cond.Message == cond.Message { + return + } + if cond.Status != condition.Status { + condition.LastTransitionTime = metav1.Now() + + } else { + condition.LastTransitionTime = cond.LastTransitionTime + } + (*conditions)[i] = condition + return + } + } + *conditions = append(*conditions, condition) +} +func (r *PlatformReconciler) reconcileComponents(ctx context.Context, platform *platformv1alpha1.Platform, components []platformv1alpha1.PlatformComponentRef, componentType string) error { + logger := r.Log.WithValues("platform", platform.Name, platform.Namespace) + logger.Info("Reconciling components", "count", len(components)) + + for _, compRef := range components { + component := &platformv1alpha1.Component{} + nn := types.NamespacedName{ + Name: compRef.ComponentReference.Name, + Namespace: r.getNamespace(compRef.ComponentReference.Namespace, platform.Namespace), + } + err := r.Client.Get(ctx, nn, component) + if client.IgnoreNotFound(err) != nil { + logger.Error(err, "Failed to get component", "component", compRef.Name) + return err + } + if err != nil { + logger.Info("Component not found, creating reference", "component", compRef.Name) + refComponent := &platformv1alpha1.Component{} + refNN := types.NamespacedName{ + Name: compRef.ComponentReference.Name, + Namespace: r.getNamespace(compRef.ComponentReference.Namespace, platform.Namespace), + } + if err := r.Client.Get(ctx, refNN, refComponent); err != nil { + logger.Error(err, "Referenced component not found", "component", compRef.Name) + r.updateComponentStatusInPlatform(ctx, platform, compRef.Name, componentType, "Failed", + fmt.Sprintf("Referenced component %s not found", compRef.Name)) + } + if err := r.setComponentOwner(ctx, platform, refComponent, compRef); err != nil { + logger.Error(err, "Failed to set component ownership", "component", compRef.Name) + } + } + r.updateComponentStatusFromComponent(ctx, platform, component, compRef.Name, componentType) + + } + return nil +} + +func (r *PlatformReconciler) updateComponentStatusFromComponent(ctx context.Context, platform *platformv1alpha1.Platform, component *platformv1alpha1.Component, componentName, componentType string) { + status := "unknown" + errorMsg := "" + + for _, condition := range component.Status.Conditions { + if condition.Type == "Ready" { + if condition.Status == metav1.ConditionTrue { + status = "Ready" + } else { + status = condition.Reason + errorMsg = condition.Message + } + break + } + } + r.updateComponentStatusInPlatform(ctx, platform, componentName, componentType, status, errorMsg) +} +func (r *PlatformReconciler) setComponentOwner(ctx context.Context, platform *platformv1alpha1.Platform, component *platformv1alpha1.Component, compRef platformv1alpha1.PlatformComponentRef) error { + + if metav1.IsControlledBy(component, platform) { + return nil + } + if err := controllerutil.SetControllerReference(platform, component, r.Scheme); err != nil { + return err + } + if platform.Spec.GlobalConfig.Labels != nil { + if component.Labels == nil { + component.Labels = make(map[string]string) + } + for k, v := range platform.Spec.GlobalConfig.Labels { + component.Labels[k] = v + } + } + if platform.Spec.GlobalConfig.Annotations != nil { + if component.Annotations == nil { + component.Annotations = make(map[string]string) + } + for k, v := range platform.Spec.GlobalConfig.Annotations { + component.Annotations[k] = v + } + } + if component.Labels == nil { + component.Labels = make(map[string]string) + } + component.Labels["platform.operator.dev/platform"] = platform.Name + component.Labels["platform.operator.dev/component"] = compRef.Name + return r.Client.Update(ctx, component) +} +func (r *PlatformReconciler) updateComponentStatusInPlatform(ctx context.Context, platform *platformv1alpha1.Platform, componentName, componentType, status, errorMsg string) { + compStatus := platformv1alpha1.DeploymentStatus{ + Name: componentName, + Status: status, + Error: errorMsg, + } + switch componentType { + case "infrastructure": + r.updateComponentStatusList(&platform.Status.Components.Infrastructure, compStatus) + case "tools": + r.updateComponentStatusList(&platform.Status.Components.Tools, compStatus) + case "agents": + r.updateComponentStatusList(&platform.Status.Components.Agents, compStatus) + } +} +func (r *PlatformReconciler) updateComponentStatusList(statusList *[]platformv1alpha1.DeploymentStatus, status platformv1alpha1.DeploymentStatus) { + for i, s := range *statusList { + if s.Name == status.Name { + (*statusList)[i].Status = status.Status + (*statusList)[i].Error = status.Error + return + } + + } + *statusList = append(*statusList, platformv1alpha1.DeploymentStatus{ + Name: status.Name, + Status: status.Status, + Error: status.Error, + }) +} +func (r *PlatformReconciler) deletePlatform(ctx context.Context, platform *platformv1alpha1.Platform) (ctrl.Result, error) { + logger := r.Log.WithValues("platform", platform.Name, platform.Namespace) + logger.Info("Deleting platform") + + allComponents := []platformv1alpha1.PlatformComponentRef{} + allComponents = append(allComponents, platform.Spec.Infrastructure...) + allComponents = append(allComponents, platform.Spec.Tools...) + allComponents = append(allComponents, platform.Spec.Agents...) + + for _, compRef := range allComponents { + + component := &platformv1alpha1.Component{} + nn := types.NamespacedName{ + Name: compRef.Name, + Namespace: r.getNamespace(compRef.ComponentReference.Namespace, platform.Namespace), + } + if err := r.Client.Get(ctx, nn, component); err != nil { + if metav1.IsControlledBy(component, platform) { + logger.Info("Deleting component", "component", component.Name) + if err := r.Client.Delete(ctx, component); err != nil { + logger.Error(err, "Failed to delete component", "component", compRef.Name) + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + } + } + controllerutil.RemoveFinalizer(platform, platformFinalizer) return ctrl.Result{}, nil } +func (r *PlatformReconciler) initializePlatformStatus(ctx context.Context, platform *platformv1alpha1.Platform) error { + platform.Status.Phase = "Initializing" + platform.Status.Components = platformv1alpha1.ComponentsStatus{ + Infrastructure: []platformv1alpha1.DeploymentStatus{}, + Tools: []platformv1alpha1.DeploymentStatus{}, + Agents: []platformv1alpha1.DeploymentStatus{}, + } + + platform.Status.Conditions = []metav1.Condition{ + { + Type: "Ready", + Status: metav1.ConditionFalse, + LastTransitionTime: metav1.Now(), + Reason: "Initializing", + Message: "Platform is being initialized", + }, + } + return r.Client.Status().Update(ctx, platform) +} +func (r *PlatformReconciler) getNamespace(providedNs, defaultNs string) string { + if providedNs != "" { + return providedNs + } + return defaultNs +} + // SetupWithManager sets up the controller with the Manager. func (r *PlatformReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&kagentioperatordevv1alpha1.Platform{}). - Named("platform"). + For(&platformv1alpha1.Platform{}). + Owns(&platformv1alpha1.Component{}). Complete(r) } diff --git a/platform-operator/internal/deployer/kubernetes/kubernetes.go b/platform-operator/internal/deployer/kubernetes/kubernetes.go index 2b7be06a..5fbc97bf 100644 --- a/platform-operator/internal/deployer/kubernetes/kubernetes.go +++ b/platform-operator/internal/deployer/kubernetes/kubernetes.go @@ -72,15 +72,49 @@ func (d *KubernetesDeployer) Update(ctx context.Context, component *platformv1al // Delete existing component func (d *KubernetesDeployer) Delete(ctx context.Context, component *platformv1alpha1.Component) error { + logger := d.Log.WithValues("component", component.Name, component.Namespace) + logger.Info("Deleting component's Kubernetes resources") + + namespace := component.Namespace + if component.Spec.Deployer.Namespace != "" { + namespace = component.Spec.Deployer.Namespace + } + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: component.Name, + Namespace: namespace, + }, + } + if err := d.Client.Delete(ctx, service); err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "Failed to delete service") + return err + } + } + + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: component.Name, + Namespace: namespace, + }, + } + if err := d.Client.Delete(ctx, deployment); err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "Failed to delete deployment") + return err + } + } + logger.Info("Component's Kubernetes resources deleted succesfully") + return nil } // Return status of the component func (d *KubernetesDeployer) GetStatus(ctx context.Context, component *platformv1alpha1.Component) (platformv1alpha1.ComponentDeploymentStatus, error) { - - return platformv1alpha1.ComponentDeploymentStatus{}, nil + return *component.Status.DeploymentStatus, nil } + func (d *KubernetesDeployer) createDeployment(ctx context.Context, component *platformv1alpha1.Component, namespace string) error { kubeSpec := component.Spec.Deployer.Kubernetes @@ -120,7 +154,7 @@ func (d *KubernetesDeployer) createDeployment(ctx context.Context, component *pl ObjectMeta: metav1.ObjectMeta{ Name: component.Name, - Namespace: component.Namespace, + Namespace: namespace, Labels: labels, }, @@ -160,7 +194,7 @@ func (d *KubernetesDeployer) createDeployment(ctx context.Context, component *pl } existingDeployment := &appsv1.Deployment{} - err := d.Client.Get(ctx, k8stypes.NamespacedName{Name: deployment.Name, Namespace: deployment.Namespace}, existingDeployment) + err := d.Client.Get(ctx, k8stypes.NamespacedName{Name: deployment.Name, Namespace: namespace}, existingDeployment) if err != nil { if errors.IsNotFound(err) { return d.Client.Create(ctx, deployment) From e5c4cfe4f2a90fa5df9d40e221aabfc47f9c0ab5 Mon Sep 17 00:00:00 2001 From: Jarek Cwiklik Date: Mon, 19 May 2025 14:51:00 -0400 Subject: [PATCH 3/3] fixed k8s deployment bugs Signed-off-by: Jarek Cwiklik --- .../api/v1alpha1/component_types.go | 23 ++++--- .../api/v1alpha1/zz_generated.deepcopy.go | 2 +- .../kagenti.operator.dev_components.yaml | 25 ++++---- .../samples/agent-component-no-build.yaml | 21 +++++++ .../config/samples/agent-component.yaml | 63 +++++++++---------- .../controller/component_controller.go | 56 ++++++++++++++--- .../controller/platform_controller.go | 6 +- .../internal/deployer/helm/helm.go | 4 ++ .../deployer/kubernetes/kubernetes.go | 16 +++-- .../internal/deployer/olm/olm.go | 4 ++ .../internal/deployer/types/deployer.go | 1 + 11 files changed, 145 insertions(+), 76 deletions(-) create mode 100644 platform-operator/config/samples/agent-component-no-build.yaml diff --git a/platform-operator/api/v1alpha1/component_types.go b/platform-operator/api/v1alpha1/component_types.go index 5e6c0e47..97bb742d 100644 --- a/platform-operator/api/v1alpha1/component_types.go +++ b/platform-operator/api/v1alpha1/component_types.go @@ -25,23 +25,22 @@ type ComponentSpec struct { // Component Types // Union pattern: only one of the following components should be specified. - Agent *AgentComponent `json:"agentComponent,omitempty"` + Agent *AgentComponent `json:"agent,omitempty"` // MCP Servers, Utilities, etc - Tool *ToolComponent `json:"toolComponent,omitempty"` + Tool *ToolComponent `json:"tool,omitempty"` // Redis, Postgresql, etc - Infra *InfraComponent `json:"infraComponent,omitempty"` + Infra *InfraComponent `json:"infra,omitempty"` // -------------------------- // Common fields for all component types + // Deployment strategy for the component: Helm, K8s manifest(deployments), OLM (operators) + Deployer DeployerSpec `json:"deployer"` // Description is a human-readable description of the component // +optional Description string `json:"description,omitempty"` - // Deployment strategy for the component: Helm, K8s manifest(deployments), OLM (operators) - Deployer DeployerSpec `json:"deployerSpec"` - // Dependencies defines other components this agent depends on // +optional Dependencies []DependencySpec `json:"dependencies,omitempty"` @@ -52,7 +51,7 @@ type AgentComponent struct { // Build configuration for building the agent from source // +optional - Build *BuildSpec `json:"buildSpec,omitempty"` + Build *BuildSpec `json:"build,omitempty"` } type ToolComponent struct { @@ -60,7 +59,7 @@ type ToolComponent struct { // Build configuration for building the tool from source // +optional - Build *BuildSpec `json:"buildSpec,omitempty"` + Build *BuildSpec `json:"build,omitempty"` // ToolType specifies the type of tool // MCP;Utility @@ -103,9 +102,9 @@ type DependencySpec struct { // DeployerSpec defines how to deploy a component type DeployerSpec struct { // Only one of the following deployment methods should be specified. - Helm *HelmSpec `json:"helmSpec,omitempty"` - Kubernetes *KubernetesSpec `json:"kubernetesSpec,omitempty"` - Olm *OlmSpec `json:"olmSpec,omitempty"` + Helm *HelmSpec `json:"helm,omitempty"` + Kubernetes *KubernetesSpec `json:"kubernetes,omitempty"` + Olm *OlmSpec `json:"olm,omitempty"` // Common deployment settings // Name of the k8s resource @@ -209,7 +208,7 @@ type HelmSpec struct { // KubernetesSpec defines Kubernetes manifest deployment type KubernetesSpec struct { - Image ImageSpec `json:"imageSpec,omitempty"` + ImageSpec ImageSpec `json:"imageSpec,omitempty"` // Resources is the compute resources required by the container // +optional diff --git a/platform-operator/api/v1alpha1/zz_generated.deepcopy.go b/platform-operator/api/v1alpha1/zz_generated.deepcopy.go index 71f10881..4b30e59d 100644 --- a/platform-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/platform-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -478,7 +478,7 @@ func (in *InfraComponent) DeepCopy() *InfraComponent { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubernetesSpec) DeepCopyInto(out *KubernetesSpec) { *out = *in - out.Image = in.Image + out.ImageSpec = in.ImageSpec in.Resources.DeepCopyInto(&out.Resources) if in.ContainerPorts != nil { in, out := &in.ContainerPorts, &out.ContainerPorts diff --git a/platform-operator/config/crd/bases/kagenti.operator.dev_components.yaml b/platform-operator/config/crd/bases/kagenti.operator.dev_components.yaml index 24fd1df5..eb760726 100644 --- a/platform-operator/config/crd/bases/kagenti.operator.dev_components.yaml +++ b/platform-operator/config/crd/bases/kagenti.operator.dev_components.yaml @@ -38,12 +38,12 @@ spec: type: object spec: properties: - agentComponent: + agent: description: |- Component Types Union pattern: only one of the following components should be specified. properties: - buildSpec: + build: description: Build configuration for building the agent from source properties: buildArgs: @@ -146,9 +146,10 @@ spec: - name type: object type: array - deployerSpec: - description: 'Deployment strategy for the component: Helm, K8s manifest(deployments), - OLM (operators)' + deployer: + description: |- + Common fields for all component types + Deployment strategy for the component: Helm, K8s manifest(deployments), OLM (operators) properties: deployAfterBuild: description: DeployAfterBuild indicates whether to automatically @@ -274,7 +275,7 @@ spec: - name type: object type: array - helmSpec: + helm: description: Only one of the following deployment methods should be specified. properties: @@ -299,7 +300,7 @@ spec: required: - chartName type: object - kubernetesSpec: + kubernetes: description: KubernetesSpec defines Kubernetes manifest deployment properties: containerPorts: @@ -509,14 +510,14 @@ spec: description: Namespace to deploy to, defaults to the namespace of the CR type: string - olmSpec: + olm: description: HelmSpec defines Helm deployment configuration type: object type: object description: description: Description is a human-readable description of the component type: string - infraComponent: + infra: description: Redis, Postgresql, etc properties: infraProvider: @@ -552,10 +553,10 @@ spec: - infraType - version type: object - toolComponent: + tool: description: MCP Servers, Utilities, etc properties: - buildSpec: + build: description: Build configuration for building the tool from source properties: buildArgs: @@ -642,7 +643,7 @@ spec: - toolType type: object required: - - deployerSpec + - deployer type: object status: description: ComponentStatus defines the observed state of Component. diff --git a/platform-operator/config/samples/agent-component-no-build.yaml b/platform-operator/config/samples/agent-component-no-build.yaml new file mode 100644 index 00000000..83595ca6 --- /dev/null +++ b/platform-operator/config/samples/agent-component-no-build.yaml @@ -0,0 +1,21 @@ +apiVersion: kagenti.operator.dev/v1alpha1 +kind: Component +metadata: + name: my-agent +spec: + description: My first agent component + agentComponent: + + deployer: + name: "my-agent-deployment" + namespace: "default" + deployAfterBuild: true + kubernetes: + imageSpec: + image: "beai-research-agent" + imageTag: "latest" + imageRegistry: "registry.cr-system.svc.cluster.local:5000" + imagePullPolicy: "IfNotPresent" + env: + - name: "LOG_LEVEL" + value: "INFO" diff --git a/platform-operator/config/samples/agent-component.yaml b/platform-operator/config/samples/agent-component.yaml index 1723205b..510f2e1a 100644 --- a/platform-operator/config/samples/agent-component.yaml +++ b/platform-operator/config/samples/agent-component.yaml @@ -1,53 +1,52 @@ -apiVersion: beeai.dev/v1 +apiVersion: kagenti.operator.dev/v1alpha1 kind: Component metadata: name: research-agent spec: + description: "A BeeAI research agent for information gathering" + # Component type (only one should be specified) agentComponent: + # Optional build specification for building from source build: - sourceRepository: "github.com/beeai/agent-examples.git" + sourceRepository: "github.com/kagenti/agent-examples.git" sourceRevision: "main" - sourceSubfolder: "beeai/research-agent" - repoUser: "beeai" + sourceSubfolder: "acp/ollama-deep-researcher" + repoUser: "cwiklik" sourceCredentials: name: "github-token-secret" buildOutput: - image: "research-agent" - imageTag: "v1.0.0" - imageRegistry: "ghcr.io/beeai" + image: "beai-research-agent" + imageTag: "latest" + imageRegistry: "registry.cr-system.svc.cluster.local:5000" cleanupAfterBuild: true # General component configuration - description: "A BeeAI research agent for information gathering" + + deployer: + name: "my-agent-deployment" + namespace: "default" + deployAfterBuild: true + kubernetes: + imageSpec: + image: "beai-research-agent" + imageTag: "latest" + imageRegistry: "registry.cr-system.svc.cluster.local:5000" + imagePullPolicy: "IfNotPresent" + resources: + limits: + cpu: "1" + memory: "2Gi" + requests: + cpu: "500m" + memory: "1Gi" + serviceType: "ClusterIP" + env: - name: LLM_MODEL value: "llama3.2:70b" - name: LLM_URL value: "http://llm-service:11434" + - # Deployment configuration - deploymentSpec: - # Deployment method (only one should be specified) - kubernetesSpec: - imageSpec: - image: "research-agent" - imageTag: "v1.0.0" - imageRegistry: "ghcr.io/beeai" - resources: - limits: - cpu: "1" - memory: "2Gi" - requests: - cpu: "500m" - memory: "1Gi" - serviceType: "ClusterIP" - - # Automatic deployment after build - deployAfterBuild: true - - # Component dependencies - dependencies: - - name: "mcp-server-weather" - type: "Tool" \ No newline at end of file diff --git a/platform-operator/internal/controller/component_controller.go b/platform-operator/internal/controller/component_controller.go index 4a209c6a..f5f01fb3 100644 --- a/platform-operator/internal/controller/component_controller.go +++ b/platform-operator/internal/controller/component_controller.go @@ -25,9 +25,12 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + //"sigs.k8s.io/controller-runtime/pkg/log" "github.com/go-logr/logr" platformv1alpha1 "github.com/kagenti/operator/platform/api/v1alpha1" @@ -54,7 +57,7 @@ const componentFinalizer = "kagenti.operator.dev/finalizer" // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Log.WithValues("controller", req.Name, req.Namespace) + logger := r.Log.WithValues("controller", req.Name, "Namespace", req.Namespace) logger.Info("Reconciling component") component := &platformv1alpha1.Component{} @@ -87,7 +90,7 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, err } if doDeploy { - logger.Info("Starting component deployment") + logger.Info("Starting component deployment --------") component.Status.DeploymentStatus.Phase = "Deploying" component.Status.DeploymentStatus.DeploymentMessage = "Deployment in progress" if err := r.Client.Status().Update(ctx, component); err != nil { @@ -114,6 +117,11 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } return ctrl.Result{RequeueAfter: time.Second * 20}, nil } + phase := "" + if component.Status.DeploymentStatus != nil { + phase = component.Status.DeploymentStatus.Phase + } + if component.Status.DeploymentStatus != nil && component.Status.DeploymentStatus.Phase == "Deploying" { if err := r.checkDeploymentStatus(ctx, component); err != nil { logger.Error(err, "Failed to check deployment status") @@ -125,7 +133,7 @@ func (r *ComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( logger.Error(err, "Failed to update component status") return ctrl.Result{}, nil } - logger.Info("Component reconciliation competed successfully") + logger.Info("Component reconciliation completed successfully", "phase", phase) return ctrl.Result{RequeueAfter: time.Second * 50}, nil } func (r *ComponentReconciler) initializeComponentStatus(ctx context.Context, component *platformv1alpha1.Component) error { @@ -214,27 +222,55 @@ func (r *ComponentReconciler) updateComponentCondition(component *platformv1alph component.Status.Conditions = append(component.Status.Conditions, condition) } func (r *ComponentReconciler) checkDeploymentStatus(ctx context.Context, component *platformv1alpha1.Component) error { - logger := r.Log.WithValues("component", component.Name, component.Namespace) + logger := r.Log.WithValues("component", component.Name, "Namespace", component.Namespace) logger.Info("Checking component status") deployer, err := r.DeployerFactory.GetDeployer(component) if err != nil { + logger.Error(err, "Failed deployer lookup - invalid component") component.Status.DeploymentStatus.Phase = "Failed" component.Status.DeploymentStatus.DeploymentMessage = "Invalid deployer for the component" err = r.Client.Status().Update(ctx, component) if err != nil { return err } - } + } + logger.Info("checkDeploymentStatus", "deployer", deployer.GetName()) ready, message, err := deployer.CheckComponentStatus(ctx, component) + if err != nil { + logger.Error(err, "Failed to check component status ") + return err + } component.Status.DeploymentStatus.DeploymentMessage = message - if ready { - component.Status.DeploymentStatus.Phase = "Ready" - } else { - component.Status.DeploymentStatus.Phase = "Deploying" + + logger.Info("checkDeploymentStatus", "phase", component.Status.DeploymentStatus.Phase) + + nn := types.NamespacedName{ + Name: component.Name, + Namespace: component.Namespace, } - return r.Client.Status().Update(ctx, component) + // Attempt to update the object with retry + err2 := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + current := &platformv1alpha1.Component{} + + if err := r.Client.Get(ctx, nn, current); err != nil { + return err + } + if ready { + current.Status.DeploymentStatus.Phase = "Ready" + current.Status.DeploymentStatus.DeploymentMessage = "Component Deployed" + } else { + current.Status.DeploymentStatus.Phase = "Deploying" + } + + return r.Client.Status().Update(ctx, current) + }) + if err2 != nil { + logger.Error(err, "Failed to update component status ") + return err + } + return nil //r.Client.Status().Update(ctx, component) } func (r *ComponentReconciler) isDeploymentNeeded(component *platformv1alpha1.Component) (bool, error) { diff --git a/platform-operator/internal/controller/platform_controller.go b/platform-operator/internal/controller/platform_controller.go index 19cfbc6e..bf9fc49d 100644 --- a/platform-operator/internal/controller/platform_controller.go +++ b/platform-operator/internal/controller/platform_controller.go @@ -42,7 +42,7 @@ type PlatformReconciler struct { const platformFinalizer = "kagenti.operator.dev/finalizer" func (r *PlatformReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Log.WithValues("platform", req.Name, req.Namespace) + logger := r.Log.WithValues("platform", req.Name, "Namespace", req.Namespace) logger.Info("Reconciling agentic platform") platform := &platformv1alpha1.Platform{} @@ -162,7 +162,7 @@ func (r *PlatformReconciler) updateCondition(conditions *[]metav1.Condition, con *conditions = append(*conditions, condition) } func (r *PlatformReconciler) reconcileComponents(ctx context.Context, platform *platformv1alpha1.Platform, components []platformv1alpha1.PlatformComponentRef, componentType string) error { - logger := r.Log.WithValues("platform", platform.Name, platform.Namespace) + logger := r.Log.WithValues("platform", platform.Name, "Namespace", platform.Namespace) logger.Info("Reconciling components", "count", len(components)) for _, compRef := range components { @@ -277,7 +277,7 @@ func (r *PlatformReconciler) updateComponentStatusList(statusList *[]platformv1a }) } func (r *PlatformReconciler) deletePlatform(ctx context.Context, platform *platformv1alpha1.Platform) (ctrl.Result, error) { - logger := r.Log.WithValues("platform", platform.Name, platform.Namespace) + logger := r.Log.WithValues("platform", platform.Name, "Namespace", platform.Namespace) logger.Info("Deleting platform") allComponents := []platformv1alpha1.PlatformComponentRef{} diff --git a/platform-operator/internal/deployer/helm/helm.go b/platform-operator/internal/deployer/helm/helm.go index 59253e7b..2b907196 100644 --- a/platform-operator/internal/deployer/helm/helm.go +++ b/platform-operator/internal/deployer/helm/helm.go @@ -19,12 +19,16 @@ type HelmDeployer struct { } func NewHelmDeployer(client client.Client, log logr.Logger, scheme *runtime.Scheme) *HelmDeployer { + log.Info("NewHelmDeployer -------------- ") return &HelmDeployer{ Client: client, Log: log, Scheme: scheme, } } +func (b *HelmDeployer) GetName() string { + return "helm" +} func (b *HelmDeployer) Deploy(ctx context.Context, component *platformv1alpha1.Component) error { return nil diff --git a/platform-operator/internal/deployer/kubernetes/kubernetes.go b/platform-operator/internal/deployer/kubernetes/kubernetes.go index 5fbc97bf..0e876875 100644 --- a/platform-operator/internal/deployer/kubernetes/kubernetes.go +++ b/platform-operator/internal/deployer/kubernetes/kubernetes.go @@ -27,15 +27,19 @@ type KubernetesDeployer struct { } func NewKubernetesDeployer(client client.Client, log logr.Logger, scheme *runtime.Scheme) *KubernetesDeployer { + log.Info("NewKubernetesDeployer -------------- ") return &KubernetesDeployer{ Client: client, Log: log, Scheme: scheme, } } +func (b *KubernetesDeployer) GetName() string { + return "kubernetes" +} func (d *KubernetesDeployer) Deploy(ctx context.Context, component *platformv1alpha1.Component) error { - logger := d.Log.WithValues("deployer", component.Name, component.Namespace) + logger := d.Log.WithValues("deployer", component.Name, "Namespace", component.Namespace) logger.Info("Deploying component with Kubernetes resources") namespace := component.Namespace @@ -72,7 +76,7 @@ func (d *KubernetesDeployer) Update(ctx context.Context, component *platformv1al // Delete existing component func (d *KubernetesDeployer) Delete(ctx context.Context, component *platformv1alpha1.Component) error { - logger := d.Log.WithValues("component", component.Name, component.Namespace) + logger := d.Log.WithValues("component", component.Name, "Namespace", component.Namespace) logger.Info("Deleting component's Kubernetes resources") namespace := component.Namespace @@ -145,9 +149,9 @@ func (d *KubernetesDeployer) createDeployment(ctx context.Context, component *pl } } image := fmt.Sprintf("%s/%s:%s", - kubeSpec.Image.ImageRegistry, - kubeSpec.Image.Image, - kubeSpec.Image.ImageTag, + kubeSpec.ImageSpec.ImageRegistry, + kubeSpec.ImageSpec.Image, + kubeSpec.ImageSpec.ImageTag, ) deployment := &appsv1.Deployment{ @@ -174,7 +178,7 @@ func (d *KubernetesDeployer) createDeployment(ctx context.Context, component *pl { Name: component.Name, Image: image, - ImagePullPolicy: corev1.PullPolicy(kubeSpec.Image.ImagePullPolicy), + ImagePullPolicy: corev1.PullPolicy(kubeSpec.ImageSpec.ImagePullPolicy), Resources: component.Spec.Deployer.Kubernetes.Resources, Env: component.Spec.Deployer.Env, Ports: containerPorts, diff --git a/platform-operator/internal/deployer/olm/olm.go b/platform-operator/internal/deployer/olm/olm.go index ffc6a110..c6805803 100644 --- a/platform-operator/internal/deployer/olm/olm.go +++ b/platform-operator/internal/deployer/olm/olm.go @@ -19,12 +19,16 @@ type OLMDeployer struct { } func NewOLMDeployer(client client.Client, log logr.Logger, scheme *runtime.Scheme) *OLMDeployer { + log.Info("NewOLMDeployer -------------- ") return &OLMDeployer{ Client: client, Log: log, Scheme: scheme, } } +func (b *OLMDeployer) GetName() string { + return "olm" +} func (b *OLMDeployer) Deploy(ctx context.Context, component *platformv1alpha1.Component) error { return nil diff --git a/platform-operator/internal/deployer/types/deployer.go b/platform-operator/internal/deployer/types/deployer.go index a6b16851..03fec9bd 100644 --- a/platform-operator/internal/deployer/types/deployer.go +++ b/platform-operator/internal/deployer/types/deployer.go @@ -7,6 +7,7 @@ import ( ) type ComponentDeployer interface { + GetName() string // Deploy component into the k8s cluster Deploy(ctx context.Context, component *platformv1alpha1.Component) error // Update existing component