From 5ee9d73b4426991fad17bd3800bcdbbec824bbd7 Mon Sep 17 00:00:00 2001 From: Jordi Gil Date: Wed, 31 Jan 2024 09:19:46 -0500 Subject: [PATCH 1/5] Disable platform e2e test due to workflow explicitly requiring persitence in production profile when it did not in the past (#378) Signed-off-by: Jordi Gil --- test/e2e/platform_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/platform_test.go b/test/e2e/platform_test.go index 24115567a..49afd5aed 100644 --- a/test/e2e/platform_test.go +++ b/test/e2e/platform_test.go @@ -117,9 +117,9 @@ var _ = Describe("Validate the persistence", Ordered, func() { } }, Entry("with both Job Service and Data Index and ephemeral persistence and the workflow in a dev profile", test.GetSonataFlowE2EPlatformServicesDirectory(), dev, ephemeral), - Entry("with both Job Service and Data Index and ephemeral persistence and the workflow in a production profile", test.GetSonataFlowE2EPlatformServicesDirectory(), production, ephemeral), + XEntry("with both Job Service and Data Index and ephemeral persistence and the workflow in a production profile", test.GetSonataFlowE2EPlatformServicesDirectory(), production, ephemeral), Entry("with both Job Service and Data Index and postgreSQL persistence and the workflow in a dev profile", test.GetSonataFlowE2EPlatformServicesDirectory(), dev, postgreSQL), - Entry("with both Job Service and Data Index and postgreSQL persistence and the workflow in a production profile", test.GetSonataFlowE2EPlatformServicesDirectory(), production, postgreSQL), + XEntry("with both Job Service and Data Index and postgreSQL persistence and the workflow in a production profile", test.GetSonataFlowE2EPlatformServicesDirectory(), production, postgreSQL), ) }) From 0a8e554eaadde5747a49742d77a322c8565fb382 Mon Sep 17 00:00:00 2001 From: Tommy Hughes IV Date: Thu, 1 Feb 2024 09:55:24 -0600 Subject: [PATCH 2/5] [KOGITO-9972] Add SonataFlowClusterPlatform CRD & Controller (#345) * SonataFlowClusterPlatform CRD & Controller Signed-off-by: Tommy Hughes * service constants cleanup Signed-off-by: Tommy Hughes * clusterplatform api descrip change Signed-off-by: Tommy Hughes --------- Signed-off-by: Tommy Hughes --- PROJECT | 8 + .../sonataflowclusterplatform_types.go | 100 +++++++++ ...sonataflowclusterplatform_types_support.go | 34 +++ .../sonataflowplatform_services_types.go | 8 +- api/v1alpha08/sonataflowplatform_types.go | 37 +++- api/v1alpha08/zz_generated.deepcopy.go | 178 ++++++++++++++- ...c.authorization.k8s.io_v1_clusterrole.yaml | 27 +++ ...taflow-operator.clusterserviceversion.yaml | 56 +++++ ...taflow.org_sonataflowclusterplatforms.yaml | 120 ++++++++++ .../sonataflow.org_sonataflowplatforms.yaml | 68 +++++- ...taflow.org_sonataflowclusterplatforms.yaml | 115 ++++++++++ .../sonataflow.org_sonataflowplatforms.yaml | 68 +++++- config/crd/kustomization.yaml | 3 + ...jection_in_sonataflowclusterplatforms.yaml | 7 + ...webhook_in_sonataflowclusterplatforms.yaml | 16 ++ ...taflow-operator.clusterserviceversion.yaml | 17 ++ config/rbac/role.yaml | 26 +++ ...sonataflowclusterplatform_editor_role.yaml | 31 +++ ...rplatform_viewer_cluster_role_binding.yaml | 13 ++ ...sonataflowclusterplatform_viewer_role.yaml | 27 +++ config/samples/kustomization.yaml | 1 + ...g_v1alpha08_sonataflowclusterplatform.yaml | 8 + controllers/clusterplatform/action.go | 50 +++++ .../clusterplatform/clusterplatform.go | 117 ++++++++++ controllers/clusterplatform/initialize.go | 118 ++++++++++ controllers/platform/k8s.go | 39 ++-- controllers/platform/platformutils.go | 16 +- controllers/platform/services/properties.go | 7 +- .../services/properties_services_test.go | 18 ++ controllers/platform/services/services.go | 159 +++++++++++-- .../common/constants/platform_services.go | 3 + .../common/properties/application_test.go | 11 +- .../sonataflowclusterplatform_controller.go | 175 +++++++++++++++ controllers/sonataflowplatform_controller.go | 120 +++++++++- .../sonataflowplatform_controller_test.go | 143 +++++++++++- main.go | 10 + operator.yaml | 209 +++++++++++++++++- ...g_v1alpha08_sonataflowclusterplatform.yaml | 25 +++ test/yaml.go | 48 +++- 39 files changed, 2119 insertions(+), 117 deletions(-) create mode 100644 api/v1alpha08/sonataflowclusterplatform_types.go create mode 100644 api/v1alpha08/sonataflowclusterplatform_types_support.go create mode 100644 bundle/manifests/sonataflow-operator-sonataflowclusterplatform-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml create mode 100644 bundle/manifests/sonataflow.org_sonataflowclusterplatforms.yaml create mode 100644 config/crd/bases/sonataflow.org_sonataflowclusterplatforms.yaml create mode 100644 config/crd/patches/cainjection_in_sonataflowclusterplatforms.yaml create mode 100644 config/crd/patches/webhook_in_sonataflowclusterplatforms.yaml create mode 100644 config/rbac/sonataflowclusterplatform_editor_role.yaml create mode 100644 config/rbac/sonataflowclusterplatform_viewer_cluster_role_binding.yaml create mode 100644 config/rbac/sonataflowclusterplatform_viewer_role.yaml create mode 100644 config/samples/sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml create mode 100644 controllers/clusterplatform/action.go create mode 100644 controllers/clusterplatform/clusterplatform.go create mode 100644 controllers/clusterplatform/initialize.go create mode 100644 controllers/sonataflowclusterplatform_controller.go create mode 100644 test/testdata/sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml diff --git a/PROJECT b/PROJECT index f32f2b3cc..6ef3543eb 100644 --- a/PROJECT +++ b/PROJECT @@ -34,4 +34,12 @@ resources: kind: SonataFlowPlatform path: github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08 version: v1alpha08 +- api: + crdVersion: v1 + controller: true + domain: org + group: sonataflow + kind: SonataFlowClusterPlatform + path: github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08 + version: v1alpha08 version: "3" diff --git a/api/v1alpha08/sonataflowclusterplatform_types.go b/api/v1alpha08/sonataflowclusterplatform_types.go new file mode 100644 index 000000000..4016e5072 --- /dev/null +++ b/api/v1alpha08/sonataflowclusterplatform_types.go @@ -0,0 +1,100 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v1alpha08 + +import ( + "github.com/apache/incubator-kie-kogito-serverless-operator/api" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // SonataFlowClusterPlatformKind is the Kind name of the SonataFlowClusterPlatform CR + SonataFlowClusterPlatformKind string = "SonataFlowClusterPlatform" + PlatformNotFoundReason string = "PlatformNotFound" +) + +// SonataFlowClusterPlatformSpec defines the desired state of SonataFlowClusterPlatform +type SonataFlowClusterPlatformSpec struct { + PlatformRef SonataFlowPlatformRef `json:"platformRef"` +} + +// SonataFlowPlatformRef defines which existing SonataFlowPlatform's supporting services should be used cluster-wide. +type SonataFlowPlatformRef struct { + // Name of the SonataFlowPlatform + //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Platform_Name" + Name string `json:"name"` + // Namespace of the SonataFlowPlatform + //+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Platform_NS" + Namespace string `json:"namespace"` +} + +// SonataFlowClusterPlatformStatus defines the observed state of SonataFlowClusterPlatform +type SonataFlowClusterPlatformStatus struct { + api.Status `json:",inline"` + // Version the operator version controlling this ClusterPlatform + //+operator-sdk:csv:customresourcedefinitions:type=status,displayName="version" + Version string `json:"version,omitempty"` +} + +func (in *SonataFlowClusterPlatformStatus) GetTopLevelConditionType() api.ConditionType { + return api.SucceedConditionType +} + +func (in *SonataFlowClusterPlatformStatus) IsReady() bool { + return in.GetTopLevelCondition().IsTrue() +} + +func (in *SonataFlowClusterPlatformStatus) GetTopLevelCondition() *api.Condition { + return in.GetCondition(in.GetTopLevelConditionType()) +} + +func (in *SonataFlowClusterPlatformStatus) Manager() api.ConditionsManager { + return api.NewConditionManager(in, api.SucceedConditionType) +} + +func (in *SonataFlowClusterPlatformStatus) IsDuplicated() bool { + cond := in.GetTopLevelCondition() + return cond.IsFalse() && cond.Reason == PlatformDuplicatedReason +} + +// SonataFlowClusterPlatform is the Schema for the sonataflowclusterplatforms API +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="Platform_Name",type=string,JSONPath=`.spec.platformRef.name` +// +kubebuilder:printcolumn:name="Platform_NS",type=string,JSONPath=`.spec.platformRef.namespace` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=='Succeed')].status` +// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=='Succeed')].reason` +type SonataFlowClusterPlatform struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec SonataFlowClusterPlatformSpec `json:"spec,omitempty"` + Status SonataFlowClusterPlatformStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// SonataFlowClusterPlatformList contains a list of SonataFlowClusterPlatform +type SonataFlowClusterPlatformList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []SonataFlowClusterPlatform `json:"items"` +} + +func init() { + SchemeBuilder.Register(&SonataFlowClusterPlatform{}, &SonataFlowClusterPlatformList{}) +} diff --git a/api/v1alpha08/sonataflowclusterplatform_types_support.go b/api/v1alpha08/sonataflowclusterplatform_types_support.go new file mode 100644 index 000000000..8c126555d --- /dev/null +++ b/api/v1alpha08/sonataflowclusterplatform_types_support.go @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package v1alpha08 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NewSonataFlowClusterPlatformList returns an empty list of ClusterPlatform objects +func NewSonataFlowClusterPlatformList() SonataFlowClusterPlatformList { + return SonataFlowClusterPlatformList{ + TypeMeta: metav1.TypeMeta{ + APIVersion: GroupVersion.String(), + Kind: SonataFlowClusterPlatformKind, + }, + } +} diff --git a/api/v1alpha08/sonataflowplatform_services_types.go b/api/v1alpha08/sonataflowplatform_services_types.go index 4c3a66108..9ecad3de9 100644 --- a/api/v1alpha08/sonataflowplatform_services_types.go +++ b/api/v1alpha08/sonataflowplatform_services_types.go @@ -14,12 +14,12 @@ package v1alpha08 -// ServicesPlatformSpec describes the desired service configuration for "prod" workflows. +// ServicesPlatformSpec describes the desired service configuration for workflows without the `sonataflow.org/profile: dev` annotation. type ServicesPlatformSpec struct { - // Deploys the Data Index service for use by "prod" profile workflows. + // Deploys the Data Index service for use by workflows without the `sonataflow.org/profile: dev` annotation. // +optional DataIndex *ServiceSpec `json:"dataIndex,omitempty"` - // Deploys the Job service for use by "prod" profile workflows. + // Deploys the Job service for use by workflows without the `sonataflow.org/profile: dev` annotation. // +optional JobService *ServiceSpec `json:"jobService,omitempty"` } @@ -27,7 +27,7 @@ type ServicesPlatformSpec struct { // ServiceSpec defines the desired state of a platform service // +k8s:openapi-gen=true type ServiceSpec struct { - // Determines whether "prod" profile workflows should be configured to use this service + // Determines whether workflows without the `sonataflow.org/profile: dev` annotation should be configured to use this service // +optional Enabled *bool `json:"enabled,omitempty"` // Persists service to a datasource of choice. Ephemeral by default. diff --git a/api/v1alpha08/sonataflowplatform_types.go b/api/v1alpha08/sonataflowplatform_types.go index fd2ba0137..0ee7e9ab7 100644 --- a/api/v1alpha08/sonataflowplatform_types.go +++ b/api/v1alpha08/sonataflowplatform_types.go @@ -41,11 +41,11 @@ type SonataFlowPlatformSpec struct { // +optional // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="DevMode" DevMode DevModePlatformSpec `json:"devMode,omitempty"` - // Services attributes for deploying supporting applications like Data Index. - // Only workflows with the proper annotation will be configured to use these service(s). - // `sonataflow.org/profile: prod` + // Services attributes for deploying supporting applications like Data Index & Job Service. + // Only workflows without the `sonataflow.org/profile: dev` annotation will be configured to use these service(s). + // Setting this will override the use of any cluster-scoped services that might be defined via `SonataFlowClusterPlatform`. // +optional - Services ServicesPlatformSpec `json:"services,omitempty"` + Services *ServicesPlatformSpec `json:"services,omitempty"` } // PlatformCluster is the kind of orchestration cluster the platform is installed into @@ -79,6 +79,35 @@ type SonataFlowPlatformStatus struct { // Info generic information related to the build //+operator-sdk:csv:customresourcedefinitions:type=status,displayName="info" Info map[string]string `json:"info,omitempty"` + // ClusterPlatformRef information related to the (optional) active SonataFlowClusterPlatform + ClusterPlatformRef *SonataFlowClusterPlatformRefStatus `json:"clusterPlatformRef,omitempty"` +} + +// SonataFlowClusterPlatformRefStatus information related to the (optional) active SonataFlowClusterPlatform +// +k8s:openapi-gen=true +type SonataFlowClusterPlatformRefStatus struct { + // Name of the active SonataFlowClusterPlatform + Name string `json:"name,omitempty"` + // PlatformRef displays which SonataFlowPlatform has been referenced by the active SonataFlowClusterPlatform + PlatformRef SonataFlowPlatformRef `json:"platformRef,omitempty"` + // Services displays which cluster-wide services are being used by this SonataFlowPlatform + Services *PlatformServicesStatus `json:"services,omitempty"` +} + +// PlatformServicesStatus displays which cluster-wide services are being used by a SonataFlowPlatform +// +k8s:openapi-gen=true +type PlatformServicesStatus struct { + // DataIndexRef displays information on the cluster-wide Data Index service + DataIndexRef *PlatformServiceRefStatus `json:"dataIndexRef,omitempty"` + // JobServiceRef displays information on the cluster-wide Job Service + JobServiceRef *PlatformServiceRefStatus `json:"jobServiceRef,omitempty"` +} + +// PlatformServiceRefStatus displays information on a cluster-wide service +// +k8s:openapi-gen=true +type PlatformServiceRefStatus struct { + // Url displays the base url of a cluster-wide service + Url string `json:"url,omitempty"` } func (in *SonataFlowPlatformStatus) GetTopLevelConditionType() api.ConditionType { diff --git a/api/v1alpha08/zz_generated.deepcopy.go b/api/v1alpha08/zz_generated.deepcopy.go index 3c24b8c61..d4449494f 100644 --- a/api/v1alpha08/zz_generated.deepcopy.go +++ b/api/v1alpha08/zz_generated.deepcopy.go @@ -360,6 +360,46 @@ func (in *PersistencePostgreSql) DeepCopy() *PersistencePostgreSql { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformServiceRefStatus) DeepCopyInto(out *PlatformServiceRefStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformServiceRefStatus. +func (in *PlatformServiceRefStatus) DeepCopy() *PlatformServiceRefStatus { + if in == nil { + return nil + } + out := new(PlatformServiceRefStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformServicesStatus) DeepCopyInto(out *PlatformServicesStatus) { + *out = *in + if in.DataIndexRef != nil { + in, out := &in.DataIndexRef, &out.DataIndexRef + *out = new(PlatformServiceRefStatus) + **out = **in + } + if in.JobServiceRef != nil { + in, out := &in.JobServiceRef, &out.JobServiceRef + *out = new(PlatformServiceRefStatus) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformServicesStatus. +func (in *PlatformServicesStatus) DeepCopy() *PlatformServicesStatus { + if in == nil { + return nil + } + out := new(PlatformServicesStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PodSpec) DeepCopyInto(out *PodSpec) { *out = *in @@ -764,6 +804,118 @@ func (in *SonataFlowBuildStatus) DeepCopy() *SonataFlowBuildStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SonataFlowClusterPlatform) DeepCopyInto(out *SonataFlowClusterPlatform) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SonataFlowClusterPlatform. +func (in *SonataFlowClusterPlatform) DeepCopy() *SonataFlowClusterPlatform { + if in == nil { + return nil + } + out := new(SonataFlowClusterPlatform) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SonataFlowClusterPlatform) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SonataFlowClusterPlatformList) DeepCopyInto(out *SonataFlowClusterPlatformList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SonataFlowClusterPlatform, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SonataFlowClusterPlatformList. +func (in *SonataFlowClusterPlatformList) DeepCopy() *SonataFlowClusterPlatformList { + if in == nil { + return nil + } + out := new(SonataFlowClusterPlatformList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SonataFlowClusterPlatformList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SonataFlowClusterPlatformRefStatus) DeepCopyInto(out *SonataFlowClusterPlatformRefStatus) { + *out = *in + out.PlatformRef = in.PlatformRef + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = new(PlatformServicesStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SonataFlowClusterPlatformRefStatus. +func (in *SonataFlowClusterPlatformRefStatus) DeepCopy() *SonataFlowClusterPlatformRefStatus { + if in == nil { + return nil + } + out := new(SonataFlowClusterPlatformRefStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SonataFlowClusterPlatformSpec) DeepCopyInto(out *SonataFlowClusterPlatformSpec) { + *out = *in + out.PlatformRef = in.PlatformRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SonataFlowClusterPlatformSpec. +func (in *SonataFlowClusterPlatformSpec) DeepCopy() *SonataFlowClusterPlatformSpec { + if in == nil { + return nil + } + out := new(SonataFlowClusterPlatformSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SonataFlowClusterPlatformStatus) DeepCopyInto(out *SonataFlowClusterPlatformStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SonataFlowClusterPlatformStatus. +func (in *SonataFlowClusterPlatformStatus) DeepCopy() *SonataFlowClusterPlatformStatus { + if in == nil { + return nil + } + out := new(SonataFlowClusterPlatformStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SonataFlowList) DeepCopyInto(out *SonataFlowList) { *out = *in @@ -855,12 +1007,31 @@ func (in *SonataFlowPlatformList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SonataFlowPlatformRef) DeepCopyInto(out *SonataFlowPlatformRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SonataFlowPlatformRef. +func (in *SonataFlowPlatformRef) DeepCopy() *SonataFlowPlatformRef { + if in == nil { + return nil + } + out := new(SonataFlowPlatformRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SonataFlowPlatformSpec) DeepCopyInto(out *SonataFlowPlatformSpec) { *out = *in in.Build.DeepCopyInto(&out.Build) out.DevMode = in.DevMode - in.Services.DeepCopyInto(&out.Services) + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = new(ServicesPlatformSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SonataFlowPlatformSpec. @@ -884,6 +1055,11 @@ func (in *SonataFlowPlatformStatus) DeepCopyInto(out *SonataFlowPlatformStatus) (*out)[key] = val } } + if in.ClusterPlatformRef != nil { + in, out := &in.ClusterPlatformRef, &out.ClusterPlatformRef + *out = new(SonataFlowClusterPlatformRefStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SonataFlowPlatformStatus. diff --git a/bundle/manifests/sonataflow-operator-sonataflowclusterplatform-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml b/bundle/manifests/sonataflow-operator-sonataflowclusterplatform-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml new file mode 100644 index 000000000..4b7af794b --- /dev/null +++ b/bundle/manifests/sonataflow-operator-sonataflowclusterplatform-viewer-role_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -0,0 +1,27 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: sonataflow-operator + app.kubernetes.io/instance: sonataflowclusterplatform-viewer-role + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: clusterrole + app.kubernetes.io/part-of: sonataflow-operator + name: sonataflow-operator-sonataflowclusterplatform-viewer-role +rules: +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms + verbs: + - get + - list + - watch +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/status + verbs: + - get diff --git a/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml b/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml index 94baa4f11..9dced8cd4 100644 --- a/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml +++ b/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml @@ -86,6 +86,19 @@ metadata: "timeout": "360s" } }, + { + "apiVersion": "sonataflow.org/v1alpha08", + "kind": "SonataFlowClusterPlatform", + "metadata": { + "name": "sonataflow-clusterplatform" + }, + "spec": { + "platformRef": { + "name": "sonataflow-platform", + "namespace": "sonataflow-operator-system" + } + } + }, { "apiVersion": "sonataflow.org/v1alpha08", "kind": "SonataFlowPlatform", @@ -167,6 +180,23 @@ spec: displayName: InnerBuild path: innerBuild version: v1alpha08 + - description: SonataFlowClusterPlatform is the Schema for the sonataflowclusterplatforms + API + displayName: Sonata Flow Cluster Platform + kind: SonataFlowClusterPlatform + name: sonataflowclusterplatforms.sonataflow.org + specDescriptors: + - description: Name of the SonataFlowPlatform + displayName: Platform_Name + path: platformRef.name + - description: Namespace of the SonataFlowPlatform + displayName: Platform_NS + path: platformRef.namespace + statusDescriptors: + - description: Version the operator version controlling this ClusterPlatform + displayName: version + path: version + version: v1alpha08 - description: SonataFlowPlatform is the descriptor for the workflow platform infrastructure. displayName: Sonata Flow Platform @@ -386,6 +416,32 @@ spec: - get - patch - update + - apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/finalizers + verbs: + - update + - apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/status + verbs: + - get + - patch + - update - apiGroups: - sonataflow.org resources: diff --git a/bundle/manifests/sonataflow.org_sonataflowclusterplatforms.yaml b/bundle/manifests/sonataflow.org_sonataflowclusterplatforms.yaml new file mode 100644 index 000000000..faa83264e --- /dev/null +++ b/bundle/manifests/sonataflow.org_sonataflowclusterplatforms.yaml @@ -0,0 +1,120 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: sonataflowclusterplatforms.sonataflow.org +spec: + group: sonataflow.org + names: + kind: SonataFlowClusterPlatform + listKind: SonataFlowClusterPlatformList + plural: sonataflowclusterplatforms + singular: sonataflowclusterplatform + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.platformRef.name + name: Platform_Name + type: string + - jsonPath: .spec.platformRef.namespace + name: Platform_NS + type: string + - jsonPath: .status.conditions[?(@.type=='Succeed')].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=='Succeed')].reason + name: Reason + type: string + name: v1alpha08 + schema: + openAPIV3Schema: + description: SonataFlowClusterPlatform is the Schema for the sonataflowclusterplatforms + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SonataFlowClusterPlatformSpec defines the desired state of + SonataFlowClusterPlatform + properties: + platformRef: + description: SonataFlowPlatformRef defines which existing SonataFlowPlatform's + supporting services should be used cluster-wide. + properties: + name: + description: Name of the SonataFlowPlatform + type: string + namespace: + description: Namespace of the SonataFlowPlatform + type: string + required: + - name + - namespace + type: object + required: + - platformRef + type: object + status: + description: SonataFlowClusterPlatformStatus defines the observed state + of SonataFlowClusterPlatform + properties: + conditions: + description: The latest available observations of a resource's current + state. + items: + description: Condition describes the common structure for conditions + in our types + properties: + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type condition for the given object + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the deployment controller. + format: int64 + type: integer + version: + description: Version the operator version controlling this ClusterPlatform + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: null + storedVersions: null diff --git a/bundle/manifests/sonataflow.org_sonataflowplatforms.yaml b/bundle/manifests/sonataflow.org_sonataflowplatforms.yaml index a8437baca..5910e8b78 100644 --- a/bundle/manifests/sonataflow.org_sonataflowplatforms.yaml +++ b/bundle/manifests/sonataflow.org_sonataflowplatforms.yaml @@ -422,17 +422,18 @@ spec: type: object services: description: 'Services attributes for deploying supporting applications - like Data Index. Only workflows with the proper annotation will - be configured to use these service(s). `sonataflow.org/profile: - prod`' + like Data Index & Job Service. Only workflows without the `sonataflow.org/profile: + dev` annotation will be configured to use these service(s). Setting + this will override the use of any cluster-scoped services that might + be defined via `SonataFlowClusterPlatform`.' properties: dataIndex: - description: Deploys the Data Index service for use by "prod" - profile workflows. + description: 'Deploys the Data Index service for use by workflows + without the `sonataflow.org/profile: dev` annotation.' properties: enabled: - description: Determines whether "prod" profile workflows should - be configured to use this service + description: 'Determines whether workflows without the `sonataflow.org/profile: + dev` annotation should be configured to use this service' type: boolean persistence: description: Persists service to a datasource of choice. Ephemeral @@ -8299,12 +8300,12 @@ spec: type: object type: object jobService: - description: Deploys the Job service for use by "prod" profile - workflows. + description: 'Deploys the Job service for use by workflows without + the `sonataflow.org/profile: dev` annotation.' properties: enabled: - description: Determines whether "prod" profile workflows should - be configured to use this service + description: 'Determines whether workflows without the `sonataflow.org/profile: + dev` annotation should be configured to use this service' type: boolean persistence: description: Persists service to a datasource of choice. Ephemeral @@ -16182,6 +16183,51 @@ spec: - kubernetes - openshift type: string + clusterPlatformRef: + description: ClusterPlatformRef information related to the (optional) + active SonataFlowClusterPlatform + properties: + name: + description: Name of the active SonataFlowClusterPlatform + type: string + platformRef: + description: PlatformRef displays which SonataFlowPlatform has + been referenced by the active SonataFlowClusterPlatform + properties: + name: + description: Name of the SonataFlowPlatform + type: string + namespace: + description: Namespace of the SonataFlowPlatform + type: string + required: + - name + - namespace + type: object + services: + description: Services displays which cluster-wide services are + being used by this SonataFlowPlatform + properties: + dataIndexRef: + description: DataIndexRef displays information on the cluster-wide + Data Index service + properties: + url: + description: Url displays the base url of a cluster-wide + service + type: string + type: object + jobServiceRef: + description: JobServiceRef displays information on the cluster-wide + Job Service + properties: + url: + description: Url displays the base url of a cluster-wide + service + type: string + type: object + type: object + type: object conditions: description: The latest available observations of a resource's current state. diff --git a/config/crd/bases/sonataflow.org_sonataflowclusterplatforms.yaml b/config/crd/bases/sonataflow.org_sonataflowclusterplatforms.yaml new file mode 100644 index 000000000..a740ddea9 --- /dev/null +++ b/config/crd/bases/sonataflow.org_sonataflowclusterplatforms.yaml @@ -0,0 +1,115 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: sonataflowclusterplatforms.sonataflow.org +spec: + group: sonataflow.org + names: + kind: SonataFlowClusterPlatform + listKind: SonataFlowClusterPlatformList + plural: sonataflowclusterplatforms + singular: sonataflowclusterplatform + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.platformRef.name + name: Platform_Name + type: string + - jsonPath: .spec.platformRef.namespace + name: Platform_NS + type: string + - jsonPath: .status.conditions[?(@.type=='Succeed')].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=='Succeed')].reason + name: Reason + type: string + name: v1alpha08 + schema: + openAPIV3Schema: + description: SonataFlowClusterPlatform is the Schema for the sonataflowclusterplatforms + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SonataFlowClusterPlatformSpec defines the desired state of + SonataFlowClusterPlatform + properties: + platformRef: + description: SonataFlowPlatformRef defines which existing SonataFlowPlatform's + supporting services should be used cluster-wide. + properties: + name: + description: Name of the SonataFlowPlatform + type: string + namespace: + description: Namespace of the SonataFlowPlatform + type: string + required: + - name + - namespace + type: object + required: + - platformRef + type: object + status: + description: SonataFlowClusterPlatformStatus defines the observed state + of SonataFlowClusterPlatform + properties: + conditions: + description: The latest available observations of a resource's current + state. + items: + description: Condition describes the common structure for conditions + in our types + properties: + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type condition for the given object + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the deployment controller. + format: int64 + type: integer + version: + description: Version the operator version controlling this ClusterPlatform + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/sonataflow.org_sonataflowplatforms.yaml b/config/crd/bases/sonataflow.org_sonataflowplatforms.yaml index 6fd5a9b58..9db303538 100644 --- a/config/crd/bases/sonataflow.org_sonataflowplatforms.yaml +++ b/config/crd/bases/sonataflow.org_sonataflowplatforms.yaml @@ -423,17 +423,18 @@ spec: type: object services: description: 'Services attributes for deploying supporting applications - like Data Index. Only workflows with the proper annotation will - be configured to use these service(s). `sonataflow.org/profile: - prod`' + like Data Index & Job Service. Only workflows without the `sonataflow.org/profile: + dev` annotation will be configured to use these service(s). Setting + this will override the use of any cluster-scoped services that might + be defined via `SonataFlowClusterPlatform`.' properties: dataIndex: - description: Deploys the Data Index service for use by "prod" - profile workflows. + description: 'Deploys the Data Index service for use by workflows + without the `sonataflow.org/profile: dev` annotation.' properties: enabled: - description: Determines whether "prod" profile workflows should - be configured to use this service + description: 'Determines whether workflows without the `sonataflow.org/profile: + dev` annotation should be configured to use this service' type: boolean persistence: description: Persists service to a datasource of choice. Ephemeral @@ -8300,12 +8301,12 @@ spec: type: object type: object jobService: - description: Deploys the Job service for use by "prod" profile - workflows. + description: 'Deploys the Job service for use by workflows without + the `sonataflow.org/profile: dev` annotation.' properties: enabled: - description: Determines whether "prod" profile workflows should - be configured to use this service + description: 'Determines whether workflows without the `sonataflow.org/profile: + dev` annotation should be configured to use this service' type: boolean persistence: description: Persists service to a datasource of choice. Ephemeral @@ -16183,6 +16184,51 @@ spec: - kubernetes - openshift type: string + clusterPlatformRef: + description: ClusterPlatformRef information related to the (optional) + active SonataFlowClusterPlatform + properties: + name: + description: Name of the active SonataFlowClusterPlatform + type: string + platformRef: + description: PlatformRef displays which SonataFlowPlatform has + been referenced by the active SonataFlowClusterPlatform + properties: + name: + description: Name of the SonataFlowPlatform + type: string + namespace: + description: Namespace of the SonataFlowPlatform + type: string + required: + - name + - namespace + type: object + services: + description: Services displays which cluster-wide services are + being used by this SonataFlowPlatform + properties: + dataIndexRef: + description: DataIndexRef displays information on the cluster-wide + Data Index service + properties: + url: + description: Url displays the base url of a cluster-wide + service + type: string + type: object + jobServiceRef: + description: JobServiceRef displays information on the cluster-wide + Job Service + properties: + url: + description: Url displays the base url of a cluster-wide + service + type: string + type: object + type: object + type: object conditions: description: The latest available observations of a resource's current state. diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index a07d24bb8..492412313 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,7 @@ resources: - bases/sonataflow.org_sonataflows.yaml - bases/sonataflow.org_sonataflowbuilds.yaml - bases/sonataflow.org_sonataflowplatforms.yaml +- bases/sonataflow.org_sonataflowclusterplatforms.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -13,6 +14,7 @@ patchesStrategicMerge: #- patches/webhook_in_workflows.yaml #- patches/webhook_in_sonataflows.yaml #- patches/webhook_in_sonataflowplatforms.yaml +#- patches/webhook_in_sonataflowclusterplatforms.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -20,6 +22,7 @@ patchesStrategicMerge: #- patches/cainjection_in_workflows.yaml #- patches/cainjection_in_sonataflowworkflows.yaml #- patches/cainjection_in_sonataflowplatforms.yaml +#- patches/cainjection_in_sonataflowclusterplatforms.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_sonataflowclusterplatforms.yaml b/config/crd/patches/cainjection_in_sonataflowclusterplatforms.yaml new file mode 100644 index 000000000..dc0274cd7 --- /dev/null +++ b/config/crd/patches/cainjection_in_sonataflowclusterplatforms.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: sonataflowclusterplatforms.sonataflow.org diff --git a/config/crd/patches/webhook_in_sonataflowclusterplatforms.yaml b/config/crd/patches/webhook_in_sonataflowclusterplatforms.yaml new file mode 100644 index 000000000..ed7b0f41e --- /dev/null +++ b/config/crd/patches/webhook_in_sonataflowclusterplatforms.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: sonataflowclusterplatforms.sonataflow.org +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/manifests/bases/sonataflow-operator.clusterserviceversion.yaml b/config/manifests/bases/sonataflow-operator.clusterserviceversion.yaml index dc1b051e1..fcd5460ea 100644 --- a/config/manifests/bases/sonataflow-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/sonataflow-operator.clusterserviceversion.yaml @@ -64,6 +64,23 @@ spec: displayName: InnerBuild path: innerBuild version: v1alpha08 + - description: SonataFlowClusterPlatform is the Schema for the sonataflowclusterplatforms + API + displayName: Sonata Flow Cluster Platform + kind: SonataFlowClusterPlatform + name: sonataflowclusterplatforms.sonataflow.org + specDescriptors: + - description: Name of the SonataFlowPlatform + displayName: Platform_Name + path: platformRef.name + - description: Namespace of the SonataFlowPlatform + displayName: Platform_NS + path: platformRef.namespace + statusDescriptors: + - description: Version the operator version controlling this ClusterPlatform + displayName: version + path: version + version: v1alpha08 - description: SonataFlowPlatform is the descriptor for the workflow platform infrastructure. displayName: Sonata Flow Platform diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 49e76885f..e09a00c21 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -31,6 +31,32 @@ rules: - get - patch - update +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/finalizers + verbs: + - update +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/status + verbs: + - get + - patch + - update - apiGroups: - sonataflow.org resources: diff --git a/config/rbac/sonataflowclusterplatform_editor_role.yaml b/config/rbac/sonataflowclusterplatform_editor_role.yaml new file mode 100644 index 000000000..26f5aa2cc --- /dev/null +++ b/config/rbac/sonataflowclusterplatform_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit sonataflowclusterplatforms. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: sonataflowclusterplatform-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: sonataflow-operator + app.kubernetes.io/part-of: sonataflow-operator + app.kubernetes.io/managed-by: kustomize + name: sonataflowclusterplatform-editor-role +rules: +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/status + verbs: + - get diff --git a/config/rbac/sonataflowclusterplatform_viewer_cluster_role_binding.yaml b/config/rbac/sonataflowclusterplatform_viewer_cluster_role_binding.yaml new file mode 100644 index 000000000..d54f94282 --- /dev/null +++ b/config/rbac/sonataflowclusterplatform_viewer_cluster_role_binding.yaml @@ -0,0 +1,13 @@ +# allow users to view SonataFlowClusterPlatforms cluster-wide +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: sonataflowclusterplatform-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: sonataflowclusterplatform-viewer-role +subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:authenticated \ No newline at end of file diff --git a/config/rbac/sonataflowclusterplatform_viewer_role.yaml b/config/rbac/sonataflowclusterplatform_viewer_role.yaml new file mode 100644 index 000000000..4a64a8811 --- /dev/null +++ b/config/rbac/sonataflowclusterplatform_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view sonataflowclusterplatforms. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: sonataflowclusterplatform-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: sonataflow-operator + app.kubernetes.io/part-of: sonataflow-operator + app.kubernetes.io/managed-by: kustomize + name: sonataflowclusterplatform-viewer-role +rules: +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms + verbs: + - get + - list + - watch +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/status + verbs: + - get diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index d4ca147ba..642faef13 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -3,4 +3,5 @@ resources: - sonataflow.org_v1alpha08_sonataflow.yaml - sonataflow.org_v1alpha08_sonataflowplatform.yaml - sonataflow.org_v1alpha08_sonataflowbuild.yaml +- sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/samples/sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml b/config/samples/sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml new file mode 100644 index 000000000..2117a7a07 --- /dev/null +++ b/config/samples/sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml @@ -0,0 +1,8 @@ +apiVersion: sonataflow.org/v1alpha08 +kind: SonataFlowClusterPlatform +metadata: + name: sonataflow-clusterplatform +spec: + platformRef: + name: sonataflow-platform + namespace: sonataflow-operator-system diff --git a/controllers/clusterplatform/action.go b/controllers/clusterplatform/action.go new file mode 100644 index 000000000..20ba9dcf0 --- /dev/null +++ b/controllers/clusterplatform/action.go @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package clusterplatform + +import ( + "context" + + "github.com/apache/incubator-kie-kogito-serverless-operator/container-builder/client" + + v08 "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" +) + +// Action --. +type Action interface { + client.Injectable + + // a user friendly name for the action + Name() string + + // returns true if the action can handle the cluster platform + CanHandle(ctx context.Context, cPlatform *v08.SonataFlowClusterPlatform) bool + + // executes the handling function + Handle(ctx context.Context, cPlatform *v08.SonataFlowClusterPlatform) error +} + +type baseAction struct { + client client.Client +} + +func (action *baseAction) InjectClient(client client.Client) { + action.client = client +} diff --git a/controllers/clusterplatform/clusterplatform.go b/controllers/clusterplatform/clusterplatform.go new file mode 100644 index 000000000..6c1ffe45e --- /dev/null +++ b/controllers/clusterplatform/clusterplatform.go @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package clusterplatform + +import ( + "context" + + "github.com/apache/incubator-kie-kogito-serverless-operator/api/metadata" + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + "github.com/apache/incubator-kie-kogito-serverless-operator/log" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" +) + +// GetActiveClusterPlatform returns the currently installed active cluster platform. +func GetActiveClusterPlatform(ctx context.Context, c ctrl.Client) (*operatorapi.SonataFlowClusterPlatform, error) { + return getClusterPlatform(ctx, c, true) +} + +// getClusterPlatform returns the currently active cluster platform or any cluster platform existing in the cluster. +func getClusterPlatform(ctx context.Context, c ctrl.Client, active bool) (*operatorapi.SonataFlowClusterPlatform, error) { + klog.V(log.D).InfoS("Finding available cluster platforms") + + lst, err := listPrimaryClusterPlatforms(ctx, c) + if err != nil { + return nil, err + } + + for _, cPlatform := range lst.Items { + if IsActive(&cPlatform) { + klog.V(log.D).InfoS("Found active cluster platform", "platform", cPlatform.Name) + return &cPlatform, nil + } + } + + if !active && len(lst.Items) > 0 { + // does not require the cluster platform to be active, just return one if present + res := lst.Items[0] + klog.V(log.D).InfoS("Found cluster platform", "platform", res.Name) + return &res, nil + } + klog.V(log.I).InfoS("No cluster platform found") + return nil, k8serrors.NewNotFound(operatorapi.Resource(operatorapi.SonataFlowClusterPlatformKind), "") +} + +// listPrimaryClusterPlatforms returns all non-secondary cluster platforms installed (only one will be active). +func listPrimaryClusterPlatforms(ctx context.Context, c ctrl.Reader) (*operatorapi.SonataFlowClusterPlatformList, error) { + lst, err := listAllClusterPlatforms(ctx, c) + if err != nil { + return nil, err + } + + filtered := &operatorapi.SonataFlowClusterPlatformList{} + for i := range lst.Items { + cPl := lst.Items[i] + if !IsSecondary(&cPl) { + filtered.Items = append(filtered.Items, cPl) + } + } + return filtered, nil +} + +// allDuplicatedClusterPlatforms returns true if every cluster platform has a "Duplicated" status set +func allDuplicatedClusterPlatforms(ctx context.Context, c ctrl.Reader) bool { + lst, err := listAllClusterPlatforms(ctx, c) + if err != nil { + return false + } + + for i := range lst.Items { + if !lst.Items[i].Status.IsDuplicated() { + return false + } + } + + return true +} + +// listAllClusterPlatforms returns all clusterplatforms installed. +func listAllClusterPlatforms(ctx context.Context, c ctrl.Reader) (*operatorapi.SonataFlowClusterPlatformList, error) { + lst := operatorapi.NewSonataFlowClusterPlatformList() + if err := c.List(ctx, &lst); err != nil { + return nil, err + } + return &lst, nil +} + +// IsActive determines if the given cluster platform is being used. +func IsActive(p *operatorapi.SonataFlowClusterPlatform) bool { + return p.Status.IsReady() && !p.Status.IsDuplicated() +} + +// IsSecondary determines if the given cluster platform is marked as secondary. +func IsSecondary(p *operatorapi.SonataFlowClusterPlatform) bool { + if l, ok := p.Annotations[metadata.SecondaryPlatformAnnotation]; ok && l == "true" { + return true + } + return false +} diff --git a/controllers/clusterplatform/initialize.go b/controllers/clusterplatform/initialize.go new file mode 100644 index 000000000..15a9b9f82 --- /dev/null +++ b/controllers/clusterplatform/initialize.go @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package clusterplatform + +import ( + "context" + "fmt" + + "github.com/apache/incubator-kie-kogito-serverless-operator/api" + "github.com/apache/incubator-kie-kogito-serverless-operator/api/metadata" + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + "github.com/apache/incubator-kie-kogito-serverless-operator/log" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" +) + +// NewInitializeAction returns an action that initializes the platform configuration when not provided by the user. +func NewInitializeAction() Action { + return &initializeAction{} +} + +type initializeAction struct { + baseAction +} + +func (action *initializeAction) Name() string { + return "initialize" +} + +func (action *initializeAction) CanHandle(ctx context.Context, cPlatform *operatorapi.SonataFlowClusterPlatform) bool { + return !cPlatform.Status.IsDuplicated() || allDuplicatedClusterPlatforms(ctx, action.client) +} + +func (action *initializeAction) Handle(ctx context.Context, cPlatform *operatorapi.SonataFlowClusterPlatform) error { + duplicate, err := action.isPrimaryDuplicate(ctx, cPlatform) + if err != nil { + return err + } + if duplicate { + // another cluster platform already present + if !cPlatform.Status.IsDuplicated() { + cPlatform.Status.Manager().MarkFalse(api.SucceedConditionType, operatorapi.PlatformDuplicatedReason, "") + } + return nil + } + cPlatform.Status.Version = metadata.SpecVersion + platformRef := cPlatform.Spec.PlatformRef + + // Check referenced platform status + platform := &operatorapi.SonataFlowPlatform{} + err = action.client.Get(ctx, types.NamespacedName{Namespace: platformRef.Namespace, Name: platformRef.Name}, platform) + if err != nil { + if k8serrors.IsNotFound(err) { + klog.V(log.D).InfoS("%s platform does not exist in %s namespace.", platformRef.Name, platformRef.Namespace) + cPlatform.Status.Manager().MarkFalse(api.SucceedConditionType, operatorapi.PlatformNotFoundReason, + fmt.Sprintf("%s platform does not exist in %s namespace.", platformRef.Name, platformRef.Namespace)) + return nil + } + return err + } + + if platform != nil { + condition := platform.Status.GetTopLevelCondition() + if condition.IsTrue() { + klog.V(log.D).InfoS("Referenced SonataFlowPlatform '%s/%s' is ready", platformRef.Namespace, platformRef.Name) + cPlatform.Status.Manager().MarkTrueWithReason(api.SucceedConditionType, "", + "Referenced SonataFlowPlatform '%s/%s' is ready", platformRef.Namespace, platformRef.Name) + } else if condition.IsFalse() { + klog.V(log.D).InfoS("Referenced SonataFlowPlatform '%s/%s' not ready", platformRef.Namespace, platformRef.Name) + cPlatform.Status.Manager().MarkFalse(api.SucceedConditionType, operatorapi.PlatformFailureReason, + "Referenced SonataFlowPlatform '%s/%s' not ready", platformRef.Namespace, platformRef.Name) + } else { + klog.V(log.D).InfoS("Waiting for referenced SonataFlowPlatform '%s/%s' to be ready", platformRef.Namespace, platformRef.Name) + cPlatform.Status.Manager().MarkUnknown(api.SucceedConditionType, operatorapi.PlatformWarmingReason, + "Waiting for referenced SonataFlowPlatform '%s/%s' to be ready", platformRef.Namespace, platformRef.Name) + } + } + + return nil +} + +// Function to double-check if there is already an active cluster platform +func (action *initializeAction) isPrimaryDuplicate(ctx context.Context, cPlatform *operatorapi.SonataFlowClusterPlatform) (bool, error) { + if IsSecondary(cPlatform) { + // Always reconcile secondary cluster platforms + return false, nil + } + platforms, err := listPrimaryClusterPlatforms(ctx, action.client) + if err != nil { + return false, err + } + for _, p := range platforms.Items { + p := p // pin + if p.Name != cPlatform.Name && IsActive(&p) { + return true, nil + } + } + + return false, nil +} diff --git a/controllers/platform/k8s.go b/controllers/platform/k8s.go index 8360f1bc9..5a853f6a7 100644 --- a/controllers/platform/k8s.go +++ b/controllers/platform/k8s.go @@ -29,14 +29,13 @@ import ( "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles/common/constants" "github.com/apache/incubator-kie-kogito-serverless-operator/log" "github.com/apache/incubator-kie-kogito-serverless-operator/utils" + kubeutil "github.com/apache/incubator-kie-kogito-serverless-operator/utils/kubernetes" "github.com/apache/incubator-kie-kogito-serverless-operator/workflowproj" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - kubeutil "github.com/apache/incubator-kie-kogito-serverless-operator/utils/kubernetes" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" ) // NewServiceAction returns an action that deploys the services. @@ -62,32 +61,36 @@ func (action *serviceAction) Handle(ctx context.Context, platform *operatorapi.S return nil, err } - if platform.Spec.Services.DataIndex != nil { - if err := createServiceComponents(ctx, action.client, platform, services.NewDataIndexHandler(platform)); err != nil { - return nil, err + if platform.Spec.Services != nil { + psDI := services.NewDataIndexHandler(platform) + if psDI.IsServiceSetInSpec() { + if err := createOrUpdateServiceComponents(ctx, action.client, platform, psDI); err != nil { + return nil, err + } } - } - if platform.Spec.Services.JobService != nil { - if err := createServiceComponents(ctx, action.client, platform, services.NewJobServiceHandler(platform)); err != nil { - return nil, err + psJS := services.NewJobServiceHandler(platform) + if psJS.IsServiceSetInSpec() { + if err := createOrUpdateServiceComponents(ctx, action.client, platform, psJS); err != nil { + return nil, err + } } } return platform, nil } -func createServiceComponents(ctx context.Context, client client.Client, platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) error { - if err := createConfigMap(ctx, client, platform, psh); err != nil { +func createOrUpdateServiceComponents(ctx context.Context, client client.Client, platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) error { + if err := createOrUpdateConfigMap(ctx, client, platform, psh); err != nil { return err } - if err := createDeployment(ctx, client, platform, psh); err != nil { + if err := createOrUpdateDeployment(ctx, client, platform, psh); err != nil { return err } - return createService(ctx, client, platform, psh) + return createOrUpdateService(ctx, client, platform, psh) } -func createDeployment(ctx context.Context, client client.Client, platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) error { +func createOrUpdateDeployment(ctx context.Context, client client.Client, platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) error { readyProbe := &corev1.Probe{ ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ @@ -193,7 +196,7 @@ func createDeployment(ctx context.Context, client client.Client, platform *opera return nil } -func createService(ctx context.Context, client client.Client, platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) error { +func createOrUpdateService(ctx context.Context, client client.Client, platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) error { lbl, selectorLbl := getLabels(platform, psh) dataSvcSpec := corev1.ServiceSpec{ Ports: []corev1.ServicePort{ @@ -241,7 +244,7 @@ func getLabels(platform *operatorapi.SonataFlowPlatform, psh services.PlatformSe return lbl, selectorLbl } -func createConfigMap(ctx context.Context, client client.Client, platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) error { +func createOrUpdateConfigMap(ctx context.Context, client client.Client, platform *operatorapi.SonataFlowPlatform, psh services.PlatformServiceHandler) error { handler, err := services.NewServiceAppPropertyHandler(psh) if err != nil { return err diff --git a/controllers/platform/platformutils.go b/controllers/platform/platformutils.go index 9249bed01..ae14c40e9 100644 --- a/controllers/platform/platformutils.go +++ b/controllers/platform/platformutils.go @@ -111,13 +111,15 @@ func setPlatformDefaults(p *operatorapi.SonataFlowPlatform, verbose bool) error } // When dataIndex object set, default to enabled if bool not set - var enable = true - if p.Spec.Services.DataIndex != nil && p.Spec.Services.DataIndex.Enabled == nil { - p.Spec.Services.DataIndex.Enabled = &enable - } - // When the JobService field has a value, default to enabled if the `Enabled` field's value is nil - if p.Spec.Services.JobService != nil && p.Spec.Services.JobService.Enabled == nil { - p.Spec.Services.JobService.Enabled = &enable + if p.Spec.Services != nil { + var enable = true + if p.Spec.Services.DataIndex != nil && p.Spec.Services.DataIndex.Enabled == nil { + p.Spec.Services.DataIndex.Enabled = &enable + } + // When the JobService field has a value, default to enabled if the `Enabled` field's value is nil + if p.Spec.Services.JobService != nil && p.Spec.Services.JobService.Enabled == nil { + p.Spec.Services.JobService.Enabled = &enable + } } setStatusAdditionalInfo(p) diff --git a/controllers/platform/services/properties.go b/controllers/platform/services/properties.go index 5c411f38c..dbe6dc3f8 100644 --- a/controllers/platform/services/properties.go +++ b/controllers/platform/services/properties.go @@ -160,7 +160,8 @@ func GenerateDataIndexWorkflowProperties(workflow *operatorapi.SonataFlow, platf props := properties.NewProperties() props.Set(constants.KogitoProcessDefinitionsEventsEnabled, "false") props.Set(constants.KogitoProcessInstancesEventsEnabled, "false") - if workflow != nil && !profiles.IsDevProfile(workflow) && dataIndexEnabled(platform) { + di := NewDataIndexHandler(platform) + if workflow != nil && !profiles.IsDevProfile(workflow) && di.IsServiceEnabled() { props.Set(constants.KogitoProcessDefinitionsEventsEnabled, "true") props.Set(constants.KogitoProcessInstancesEventsEnabled, "true") di := NewDataIndexHandler(platform) @@ -183,8 +184,8 @@ func GenerateJobServiceWorkflowProperties(workflow *operatorapi.SonataFlow, plat props := properties.NewProperties() props.Set(constants.JobServiceRequestEventsConnector, constants.QuarkusHTTP) props.Set(constants.JobServiceRequestEventsURL, fmt.Sprintf("%s://localhost/v2/jobs/events", constants.JobServiceURLProtocol)) - if workflow != nil && !profiles.IsDevProfile(workflow) && jobServiceEnabled(platform) { - js := NewJobServiceHandler(platform) + js := NewJobServiceHandler(platform) + if workflow != nil && !profiles.IsDevProfile(workflow) && js.IsServiceEnabled() { p, err := js.GenerateWorkflowProperties() if err != nil { return nil, err diff --git a/controllers/platform/services/properties_services_test.go b/controllers/platform/services/properties_services_test.go index 90da80837..9cdae2e9f 100644 --- a/controllers/platform/services/properties_services_test.go +++ b/controllers/platform/services/properties_services_test.go @@ -173,6 +173,9 @@ func generatePlatform(opts ...plfmOptionFn) *operatorapi.SonataFlowPlatform { func setJobServiceEnabledValue(v *bool) plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.JobService == nil { p.Spec.Services.JobService = &operatorapi.ServiceSpec{} } @@ -182,6 +185,9 @@ func setJobServiceEnabledValue(v *bool) plfmOptionFn { func setDataIndexEnabledValue(v *bool) plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.DataIndex == nil { p.Spec.Services.DataIndex = &operatorapi.ServiceSpec{} } @@ -191,6 +197,9 @@ func setDataIndexEnabledValue(v *bool) plfmOptionFn { func emptyDataIndexServiceSpec() plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.DataIndex == nil { p.Spec.Services.DataIndex = &operatorapi.ServiceSpec{} } @@ -199,6 +208,9 @@ func emptyDataIndexServiceSpec() plfmOptionFn { func emptyJobServiceSpec() plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.JobService == nil { p.Spec.Services.JobService = &operatorapi.ServiceSpec{} } @@ -219,6 +231,9 @@ func setPlatformName(name string) plfmOptionFn { func setJobServiceJDBC(jdbc string) plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.JobService == nil { p.Spec.Services.JobService = &operatorapi.ServiceSpec{} } @@ -234,6 +249,9 @@ func setJobServiceJDBC(jdbc string) plfmOptionFn { func setDataIndexJDBC(jdbc string) plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.DataIndex == nil { p.Spec.Services.DataIndex = &operatorapi.ServiceSpec{} } diff --git a/controllers/platform/services/services.go b/controllers/platform/services/services.go index c04b4eb32..e49679e5b 100644 --- a/controllers/platform/services/services.go +++ b/controllers/platform/services/services.go @@ -72,6 +72,22 @@ type PlatformServiceHandler interface { GenerateWorkflowProperties() (*properties.Properties, error) // GenerateServiceProperties returns a property object that contains the application properties required by the service deployment GenerateServiceProperties() (*properties.Properties, error) + + // IsServiceSetInSpec returns true if the service is set in the spec. + IsServiceSetInSpec() bool + // IsServiceEnabledInSpec returns true if the service is enabled in the spec. + IsServiceEnabledInSpec() bool + // GetLocalServiceBaseUrl returns the base url of the local service + GetLocalServiceBaseUrl() string + // GetServiceBaseUrl returns the base url of the service, based on whether using local or cluster-scoped service. + GetServiceBaseUrl() string + // GetServiceUrl returns the service url, based on whether using local or cluster-scoped service. + GetServiceUrl() string + // IsServiceEnabled returns true if the service is enabled in either the spec or the status.clusterPlatformRef. + IsServiceEnabled() bool + // SetServiceUrlInStatus sets the service url in status. if reconciled instance does not have service set in spec AND + // if cluster referenced platform has said service enabled, use the cluster platform's service + SetServiceUrlInStatus(clusterRefPlatform *operatorapi.SonataFlowPlatform) } type DataIndexHandler struct { @@ -102,6 +118,56 @@ func (d DataIndexHandler) GetServiceName() string { return fmt.Sprintf("%s-%s", d.platform.Name, constants.DataIndexServiceName) } +func (d DataIndexHandler) SetServiceUrlInStatus(clusterRefPlatform *operatorapi.SonataFlowPlatform) { + psDI := NewDataIndexHandler(clusterRefPlatform) + if !isServicesSet(d.platform) && psDI.IsServiceEnabledInSpec() { + if d.platform.Status.ClusterPlatformRef != nil { + if d.platform.Status.ClusterPlatformRef.Services == nil { + d.platform.Status.ClusterPlatformRef.Services = &operatorapi.PlatformServicesStatus{} + } + d.platform.Status.ClusterPlatformRef.Services.DataIndexRef = &operatorapi.PlatformServiceRefStatus{ + Url: psDI.GetLocalServiceBaseUrl(), + } + } + } +} + +func (d DataIndexHandler) IsServiceSetInSpec() bool { + return isDataIndexSet(d.platform) +} + +func (d DataIndexHandler) IsServiceEnabledInSpec() bool { + return isDataIndexEnabled(d.platform) +} + +func (d DataIndexHandler) isServiceEnabledInStatus() bool { + return d.platform != nil && d.platform.Status.ClusterPlatformRef != nil && + d.platform.Status.ClusterPlatformRef.Services != nil && d.platform.Status.ClusterPlatformRef.Services.DataIndexRef != nil && + !isServicesSet(d.platform) +} + +func (d DataIndexHandler) IsServiceEnabled() bool { + return d.IsServiceEnabledInSpec() || d.isServiceEnabledInStatus() +} + +func (d DataIndexHandler) GetServiceUrl() string { + return d.GetServiceBaseUrl() + constants.KogitoProcessInstancesEventsPath +} + +func (d DataIndexHandler) GetServiceBaseUrl() string { + if d.IsServiceEnabledInSpec() { + return d.GetLocalServiceBaseUrl() + } + if d.isServiceEnabledInStatus() { + return d.platform.Status.ClusterPlatformRef.Services.DataIndexRef.Url + } + return "" +} + +func (d DataIndexHandler) GetLocalServiceBaseUrl() string { + return generateServiceURL(constants.KogitoServiceURLProtocol, d.platform.Namespace, d.GetServiceName()) +} + func (d DataIndexHandler) GetEnvironmentVariables() []corev1.EnvVar { return []corev1.EnvVar{ { @@ -169,17 +235,16 @@ func (d DataIndexHandler) GetServiceCmName() string { func (d DataIndexHandler) GenerateWorkflowProperties() (*properties.Properties, error) { props := properties.NewProperties() - if d.platform.Spec.Services.DataIndex != nil { - dataIndexUrl := generateServiceURL(constants.KogitoProcessEventsProtocol, d.platform.Namespace, d.GetServiceName()) - props.Set(constants.KogitoProcessDefinitionsEventsURL, fmt.Sprintf("%s/definitions", dataIndexUrl)) - props.Set(constants.KogitoProcessInstancesEventsURL, fmt.Sprintf("%s/processes", dataIndexUrl)) + if d.IsServiceEnabled() { + props.Set(constants.KogitoProcessDefinitionsEventsURL, d.GetServiceBaseUrl()+constants.KogitoProcessDefinitionsEventsPath) + props.Set(constants.KogitoProcessInstancesEventsURL, d.GetServiceUrl()) } return props, nil } func (d DataIndexHandler) GenerateServiceProperties() (*properties.Properties, error) { props := properties.NewProperties() - props.Set(constants.KogitoServiceURLProperty, generateServiceURL(constants.KogitoServiceURLProtocol, d.platform.Namespace, d.GetServiceName())) + props.Set(constants.KogitoServiceURLProperty, d.GetLocalServiceBaseUrl()) props.Set(constants.DataIndexKafkaSmallRyeHealthProperty, "false") return props, nil } @@ -216,6 +281,56 @@ func (j JobServiceHandler) GetServiceCmName() string { return fmt.Sprintf("%s-props", j.GetServiceName()) } +func (j JobServiceHandler) SetServiceUrlInStatus(clusterRefPlatform *operatorapi.SonataFlowPlatform) { + psJS := NewJobServiceHandler(clusterRefPlatform) + if !isServicesSet(j.platform) && psJS.IsServiceEnabledInSpec() { + if j.platform.Status.ClusterPlatformRef != nil { + if j.platform.Status.ClusterPlatformRef.Services == nil { + j.platform.Status.ClusterPlatformRef.Services = &operatorapi.PlatformServicesStatus{} + } + j.platform.Status.ClusterPlatformRef.Services.JobServiceRef = &operatorapi.PlatformServiceRefStatus{ + Url: psJS.GetLocalServiceBaseUrl(), + } + } + } +} + +func (j JobServiceHandler) IsServiceSetInSpec() bool { + return isJobServiceSet(j.platform) +} + +func (j JobServiceHandler) IsServiceEnabledInSpec() bool { + return isJobServiceEnabled(j.platform) +} + +func (j JobServiceHandler) isServiceEnabledInStatus() bool { + return j.platform != nil && j.platform.Status.ClusterPlatformRef != nil && + j.platform.Status.ClusterPlatformRef.Services != nil && j.platform.Status.ClusterPlatformRef.Services.JobServiceRef != nil && + !isServicesSet(j.platform) +} + +func (j JobServiceHandler) IsServiceEnabled() bool { + return j.IsServiceEnabledInSpec() || j.isServiceEnabledInStatus() +} + +func (j JobServiceHandler) GetServiceUrl() string { + return j.GetServiceBaseUrl() + constants.JobServiceURLPath +} + +func (j JobServiceHandler) GetServiceBaseUrl() string { + if j.IsServiceEnabledInSpec() { + return j.GetLocalServiceBaseUrl() + } + if j.isServiceEnabledInStatus() { + return j.platform.Status.ClusterPlatformRef.Services.JobServiceRef.Url + } + return "" +} + +func (j JobServiceHandler) GetLocalServiceBaseUrl() string { + return generateServiceURL(constants.JobServiceURLProtocol, j.platform.Namespace, j.GetServiceName()) +} + func (j JobServiceHandler) GetEnvironmentVariables() []corev1.EnvVar { return []corev1.EnvVar{ { @@ -277,17 +392,17 @@ func (j JobServiceHandler) GenerateServiceProperties() (*properties.Properties, props.Set(constants.JobServiceKafkaSmallRyeHealthProperty, "false") // add data source reactive URL jspec := j.platform.Spec.Services.JobService - if jspec != nil && jspec.Persistence != nil && jspec.Persistence.PostgreSql != nil { + if j.IsServiceSetInSpec() && jspec.Persistence != nil && jspec.Persistence.PostgreSql != nil { dataSourceReactiveURL, err := generateReactiveURL(jspec.Persistence.PostgreSql, j.GetServiceName(), j.platform.Namespace, constants.DefaultDatabaseName, constants.DefaultPostgreSQLPort) if err != nil { return nil, err } props.Set(constants.JobServiceDataSourceReactiveURL, dataSourceReactiveURL) } - if dataIndexEnabled(j.platform) { + if isDataIndexEnabled(j.platform) { di := NewDataIndexHandler(j.platform) props.Set(constants.JobServiceStatusChangeEvents, "true") - props.Set(constants.JobServiceStatusChangeEventsURL, fmt.Sprintf("%s/jobs", generateServiceURL(constants.KogitoProcessEventsProtocol, j.platform.Namespace, di.GetServiceName()))) + props.Set(constants.JobServiceStatusChangeEventsURL, di.GetLocalServiceBaseUrl()+"/jobs") } props.Sort() return props, nil @@ -295,17 +410,33 @@ func (j JobServiceHandler) GenerateServiceProperties() (*properties.Properties, func (j JobServiceHandler) GenerateWorkflowProperties() (*properties.Properties, error) { props := properties.NewProperties() - props.Set(constants.JobServiceRequestEventsURL, fmt.Sprintf("%s/v2/jobs/events", generateServiceURL(constants.KogitoProcessEventsProtocol, j.platform.Namespace, j.GetServiceName()))) + if j.IsServiceEnabled() { + // add data source reactive URL + props.Set(constants.JobServiceRequestEventsURL, j.GetServiceUrl()) + } return props, nil } -func dataIndexEnabled(platform *operatorapi.SonataFlowPlatform) bool { - return platform != nil && platform.Spec.Services.DataIndex != nil && - platform.Spec.Services.DataIndex.Enabled != nil && *platform.Spec.Services.DataIndex.Enabled +func isDataIndexEnabled(platform *operatorapi.SonataFlowPlatform) bool { + return isDataIndexSet(platform) && platform.Spec.Services.DataIndex.Enabled != nil && + *platform.Spec.Services.DataIndex.Enabled +} + +func isJobServiceEnabled(platform *operatorapi.SonataFlowPlatform) bool { + return isJobServiceSet(platform) && platform.Spec.Services.JobService.Enabled != nil && + *platform.Spec.Services.JobService.Enabled +} + +func isDataIndexSet(platform *operatorapi.SonataFlowPlatform) bool { + return isServicesSet(platform) && platform.Spec.Services.DataIndex != nil +} + +func isJobServiceSet(platform *operatorapi.SonataFlowPlatform) bool { + return isServicesSet(platform) && platform.Spec.Services.JobService != nil } -func jobServiceEnabled(platform *operatorapi.SonataFlowPlatform) bool { - return platform != nil && platform.Spec.Services.JobService != nil && platform.Spec.Services.JobService.Enabled != nil && *platform.Spec.Services.JobService.Enabled +func isServicesSet(platform *operatorapi.SonataFlowPlatform) bool { + return platform != nil && platform.Spec.Services != nil } func generateServiceURL(protocol string, namespace string, name string) string { diff --git a/controllers/profiles/common/constants/platform_services.go b/controllers/profiles/common/constants/platform_services.go index 99d295633..3db52e16c 100644 --- a/controllers/profiles/common/constants/platform_services.go +++ b/controllers/profiles/common/constants/platform_services.go @@ -32,12 +32,15 @@ const ( JobServiceStatusChangeEventsURL = "mp.messaging.outgoing.kogito-job-service-job-status-events-http.url" JobServiceURLProtocol = "http" JobServiceDataSourceReactiveURL = "quarkus.datasource.reactive.url" + JobServiceURLPath = "/v2/jobs/events" KogitoProcessEventsProtocol = "http" KogitoProcessInstancesEventsURL = "mp.messaging.outgoing.kogito-processinstances-events.url" KogitoProcessInstancesEventsEnabled = "kogito.events.processinstances.enabled" + KogitoProcessInstancesEventsPath = "/processes" KogitoProcessDefinitionsEventsURL = "mp.messaging.outgoing.kogito-processdefinitions-events.url" KogitoProcessDefinitionsEventsEnabled = "kogito.events.processdefinitions.enabled" + KogitoProcessDefinitionsEventsPath = "/definitions" KogitoUserTasksEventsEnabled = "kogito.events.usertasks.enabled" KogitoEventsVariablesEnabled = "kogito.events.variables.enabled" KogitoServiceURLProperty = "kogito.service.url" diff --git a/controllers/profiles/common/properties/application_test.go b/controllers/profiles/common/properties/application_test.go index a4c5dcf33..2a95e9674 100644 --- a/controllers/profiles/common/properties/application_test.go +++ b/controllers/profiles/common/properties/application_test.go @@ -200,7 +200,7 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { platform := test.GetBasePlatform() platform.Namespace = ns platform.Spec = operatorapi.SonataFlowPlatformSpec{ - Services: operatorapi.ServicesPlatformSpec{ + Services: &operatorapi.ServicesPlatformSpec{ DataIndex: &operatorapi.ServiceSpec{ Enabled: &enabled, }, @@ -604,6 +604,9 @@ func generatePlatform(opts ...plfmOptionFn) *operatorapi.SonataFlowPlatform { func setJobServiceEnabledValue(v *bool) plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.JobService == nil { p.Spec.Services.JobService = &operatorapi.ServiceSpec{} } @@ -613,6 +616,9 @@ func setJobServiceEnabledValue(v *bool) plfmOptionFn { func setDataIndexEnabledValue(v *bool) plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.DataIndex == nil { p.Spec.Services.DataIndex = &operatorapi.ServiceSpec{} } @@ -634,6 +640,9 @@ func setPlatformName(name string) plfmOptionFn { func setJobServiceJDBC(jdbc string) plfmOptionFn { return func(p *operatorapi.SonataFlowPlatform) { + if p.Spec.Services == nil { + p.Spec.Services = &operatorapi.ServicesPlatformSpec{} + } if p.Spec.Services.JobService == nil { p.Spec.Services.JobService = &operatorapi.ServiceSpec{} } diff --git a/controllers/sonataflowclusterplatform_controller.go b/controllers/sonataflowclusterplatform_controller.go new file mode 100644 index 000000000..a511bbafb --- /dev/null +++ b/controllers/sonataflowclusterplatform_controller.go @@ -0,0 +1,175 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package controllers + +import ( + "context" + "fmt" + "time" + + "github.com/apache/incubator-kie-kogito-serverless-operator/api" + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + clientr "github.com/apache/incubator-kie-kogito-serverless-operator/container-builder/client" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/clusterplatform" + "github.com/apache/incubator-kie-kogito-serverless-operator/log" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + ctrlrun "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// SonataFlowClusterPlatformReconciler reconciles a SonataFlowClusterPlatform object +type SonataFlowClusterPlatformReconciler struct { + // This Client, initialized using mgr.Client() above, is a split Client + // that reads objects from the cache and writes to the API server + ctrl.Client + // Non-caching Client + Reader ctrl.Reader + Scheme *runtime.Scheme + Config *rest.Config + Recorder record.EventRecorder +} + +//+kubebuilder:rbac:groups=sonataflow.org,resources=sonataflowclusterplatforms,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=sonataflow.org,resources=sonataflowclusterplatforms/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=sonataflow.org,resources=sonataflowclusterplatforms/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 SonataFlowClusterPlatform 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.13.0/pkg/reconcile +func (r *SonataFlowClusterPlatformReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + + // Fetch the SonataFlowClusterPlatform instance + var instance operatorapi.SonataFlowClusterPlatform + + err := r.Client.Get(ctx, req.NamespacedName, &instance) + if err != nil { + if errors.IsNotFound(err) { + return reconcile.Result{}, nil + } + klog.V(log.E).ErrorS(err, "Failed to get SonataFlowClusterPlatform") + return reconcile.Result{}, err + } + + instance.Status.Manager().InitializeConditions() + + cli, _ := clientr.FromCtrlClientSchemeAndConfig(r.Client, r.Scheme, r.Config) + action := clusterplatform.NewInitializeAction() + action.InjectClient(cli) + klog.V(log.I).InfoS("Invoking action", "Name", action.Name()) + + target := instance.DeepCopy() + + if action.CanHandle(ctx, target) { + if err = action.Handle(ctx, target); err != nil { + target.Status.Manager().MarkFalse(api.SucceedConditionType, operatorapi.PlatformFailureReason, err.Error()) + if err := r.Client.Status().Patch(ctx, target, ctrl.MergeFrom(&instance)); err != nil { + return reconcile.Result{}, err + } + r.Recorder.Event(&instance, corev1.EventTypeWarning, "Failed", fmt.Sprintf("Failed to update SonataFlowClusterPlaform: %s", err)) + return reconcile.Result{}, err + } + + if target != nil { + target.Status.ObservedGeneration = instance.Generation + + if err := r.Client.Status().Patch(ctx, target, ctrl.MergeFrom(&instance)); err != nil { + r.Recorder.Event(&instance, corev1.EventTypeNormal, "Status Updated", fmt.Sprintf("Updated cluster platform condition %s", instance.Status.GetTopLevelCondition())) + return reconcile.Result{}, err + } + } + + // handle one action at time so the resource + // is always at its latest state + r.Recorder.Event(&instance, corev1.EventTypeNormal, "Updated", fmt.Sprintf("Updated cluster platform condition to %s", instance.Status.GetTopLevelCondition())) + + if target != nil && target.Status.IsReady() { + return reconcile.Result{}, nil + } + + // Requeue + return reconcile.Result{ + RequeueAfter: 5 * time.Second, + }, nil + } + + return reconcile.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *SonataFlowClusterPlatformReconciler) SetupWithManager(mgr ctrlrun.Manager) error { + return ctrlrun.NewControllerManagedBy(mgr). + For(&operatorapi.SonataFlowClusterPlatform{}). + Watches(&operatorapi.SonataFlowPlatform{}, handler.EnqueueRequestsFromMapFunc(r.mapPlatformToClusterPlatformRequests)). + Watches(&operatorapi.SonataFlowClusterPlatform{}, handler.EnqueueRequestsFromMapFunc(r.mapClusterPlatformToClusterPlatformRequests)). + Complete(r) +} + +// if actively referenced sonataflowplatform object is changed, reconcile the active SonataFlowClusterPlatform. +func (r *SonataFlowClusterPlatformReconciler) mapPlatformToClusterPlatformRequests(ctx context.Context, object client.Object) []reconcile.Request { + sfcPlatform, err := clusterplatform.GetActiveClusterPlatform(ctx, r.Client) + if err != nil && !errors.IsNotFound(err) { + klog.V(log.E).ErrorS(err, "Failed to get active SonataFlowClusterPlatform") + return nil + } + + if sfcPlatform != nil { + sfpcRefNsName := types.NamespacedName{Namespace: sfcPlatform.Spec.PlatformRef.Namespace, Name: sfcPlatform.Spec.PlatformRef.Name} + if client.ObjectKeyFromObject(object) == sfpcRefNsName { + return []reconcile.Request{{NamespacedName: client.ObjectKeyFromObject(sfcPlatform)}} + } + } + return nil +} + +// if active sonataflowclusterplatform is changed, reconcile other SonataFlowClusterPlatforms. +func (r *SonataFlowClusterPlatformReconciler) mapClusterPlatformToClusterPlatformRequests(ctx context.Context, object client.Object) []reconcile.Request { + sfcPlatform := object.(*operatorapi.SonataFlowClusterPlatform) + var requests []reconcile.Request + if sfcPlatform != nil && clusterplatform.IsActive(sfcPlatform) { + var scpList operatorapi.SonataFlowClusterPlatformList + if err := r.List(ctx, &scpList); err != nil { + klog.V(log.E).ErrorS(err, "Could not list SonataFlowClusterPlatforms. "+ + "SonataFlowClusterPlatforms affected by changes to the active SonataFlowClusterPlatform %s will not be reconciled.", + sfcPlatform.Name) + return nil + } + + scpNamespacedName := client.ObjectKeyFromObject(sfcPlatform) + for _, cPlatform := range scpList.Items { + namespacedName := client.ObjectKeyFromObject(&cPlatform) + // this check is required so that the active clusterplatform object doesn't reconcile + if scpNamespacedName != namespacedName { + requests = append(requests, reconcile.Request{NamespacedName: namespacedName}) + } + } + } + return requests +} diff --git a/controllers/sonataflowplatform_controller.go b/controllers/sonataflowplatform_controller.go index 595b8339b..c0ccd3991 100644 --- a/controllers/sonataflowplatform_controller.go +++ b/controllers/sonataflowplatform_controller.go @@ -24,26 +24,26 @@ import ( "fmt" "time" + "github.com/apache/incubator-kie-kogito-serverless-operator/api" + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + clientr "github.com/apache/incubator-kie-kogito-serverless-operator/container-builder/client" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/clusterplatform" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/platform" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/platform/services" + "github.com/apache/incubator-kie-kogito-serverless-operator/log" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/apache/incubator-kie-kogito-serverless-operator/api" - - clientr "github.com/apache/incubator-kie-kogito-serverless-operator/container-builder/client" - - "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/platform" - ctrlrun "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ctrl "sigs.k8s.io/controller-runtime/pkg/client" - - operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" - "github.com/apache/incubator-kie-kogito-serverless-operator/log" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // SonataFlowPlatformReconciler reconciles a SonataFlowPlatform object @@ -114,6 +114,10 @@ func (r *SonataFlowPlatformReconciler) Reconcile(ctx context.Context, req reconc target := instance.DeepCopy() + if err = r.SonataFlowPlatformUpdateStatus(ctx, req, target); err != nil { + return reconcile.Result{}, err + } + for _, a := range actions { cli, _ := clientr.FromCtrlClientSchemeAndConfig(r.Client, r.Scheme, r.Config) a.InjectClient(cli) @@ -166,6 +170,51 @@ func (r *SonataFlowPlatformReconciler) Reconcile(ctx context.Context, req reconc } +// If an active cluster platform exists, update platform.Status accordingly +func (r *SonataFlowPlatformReconciler) SonataFlowPlatformUpdateStatus(ctx context.Context, req reconcile.Request, target *operatorapi.SonataFlowPlatform) error { + // Fetch the active SonataFlowClusterPlatform instance + sfcPlatform, err := clusterplatform.GetActiveClusterPlatform(ctx, r.Client) + if err != nil && !errors.IsNotFound(err) { + klog.V(log.E).ErrorS(err, "Failed to get active SonataFlowClusterPlatform") + return err + } + + if sfcPlatform != nil { + sfPlatform := &operatorapi.SonataFlowPlatform{} + + platformRef := sfcPlatform.Spec.PlatformRef + namespacedName := types.NamespacedName{Namespace: platformRef.Namespace, Name: platformRef.Name} + if req.NamespacedName == namespacedName { + sfPlatform = target.DeepCopy() + } else { + // retrieve referenced platform object + err := r.Reader.Get(ctx, namespacedName, sfPlatform) + if err != nil && !errors.IsNotFound(err) { + klog.V(log.E).ErrorS(err, "Failed to get referenced SonataFlowPlatform", namespacedName) + return err + } + } + + target.Status.ClusterPlatformRef = &operatorapi.SonataFlowClusterPlatformRefStatus{ + Name: sfcPlatform.Name, + PlatformRef: operatorapi.SonataFlowPlatformRef{ + Name: platformRef.Name, + Namespace: platformRef.Namespace, + }, + } + + tpsDI := services.NewDataIndexHandler(target) + tpsDI.SetServiceUrlInStatus(sfPlatform) + + tpsJS := services.NewJobServiceHandler(target) + tpsJS.SetServiceUrlInStatus(sfPlatform) + } else { + target.Status.ClusterPlatformRef = nil + } + + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *SonataFlowPlatformReconciler) SetupWithManager(mgr ctrlrun.Manager) error { return ctrlrun.NewControllerManagedBy(mgr). @@ -173,5 +222,54 @@ func (r *SonataFlowPlatformReconciler) SetupWithManager(mgr ctrlrun.Manager) err Owns(&appsv1.Deployment{}). Owns(&corev1.Service{}). Owns(&corev1.ConfigMap{}). + Watches(&operatorapi.SonataFlowPlatform{}, handler.EnqueueRequestsFromMapFunc(r.mapPlatformToPlatformRequests)). + Watches(&operatorapi.SonataFlowClusterPlatform{}, handler.EnqueueRequestsFromMapFunc(r.mapClusterPlatformToPlatformRequests)). Complete(r) } + +// if active clusterplatform object is changed, reconcile all SonataFlowPlatforms in the cluster. +func (r *SonataFlowPlatformReconciler) mapClusterPlatformToPlatformRequests(ctx context.Context, object client.Object) []reconcile.Request { + sfcPlatform := object.(*operatorapi.SonataFlowClusterPlatform) + if sfcPlatform != nil && clusterplatform.IsActive(sfcPlatform) { + return r.platformRequests(ctx, sfcPlatform, true) + } + return nil +} + +// if actively referenced sonataflowplatform is changed, reconcile other SonataFlowPlatforms in the cluster. +func (r *SonataFlowPlatformReconciler) mapPlatformToPlatformRequests(ctx context.Context, object client.Object) []reconcile.Request { + platform := object.(*operatorapi.SonataFlowPlatform) + sfcPlatform, err := clusterplatform.GetActiveClusterPlatform(ctx, r.Client) + if err != nil && !errors.IsNotFound(err) { + klog.V(log.E).ErrorS(err, "Failed to get active SonataFlowClusterPlatform") + return nil + } + + if sfcPlatform != nil { + sfpcRefNsName := types.NamespacedName{Namespace: sfcPlatform.Spec.PlatformRef.Namespace, Name: sfcPlatform.Spec.PlatformRef.Name} + if client.ObjectKeyFromObject(platform) == sfpcRefNsName { + return r.platformRequests(ctx, sfcPlatform, false) + } + } + return nil +} + +func (r *SonataFlowPlatformReconciler) platformRequests(ctx context.Context, sfcPlatform *operatorapi.SonataFlowClusterPlatform, allPlatforms bool) []reconcile.Request { + var plList operatorapi.SonataFlowPlatformList + if err := r.List(ctx, &plList, client.InNamespace("")); err != nil { + klog.V(log.E).ErrorS(err, "could not list SonataFlowPlatforms. "+ + "SonataFlowPlatforms affected by changes to the active SonataFlowPlatform or SonataFlowClusterPlatform object will not be reconciled.") + return nil + } + + sfpcRefNsName := types.NamespacedName{Namespace: sfcPlatform.Spec.PlatformRef.Namespace, Name: sfcPlatform.Spec.PlatformRef.Name} + var requests []reconcile.Request + for _, platform := range plList.Items { + sfpNsName := client.ObjectKeyFromObject(&platform) + // this check is required so that the cluster-referenced platform object doesn't infinitely reconcile + if sfpNsName != sfpcRefNsName || allPlatforms { + requests = append(requests, reconcile.Request{NamespacedName: sfpNsName}) + } + } + return requests +} diff --git a/controllers/sonataflowplatform_controller_test.go b/controllers/sonataflowplatform_controller_test.go index db0ba0805..5594d6449 100644 --- a/controllers/sonataflowplatform_controller_test.go +++ b/controllers/sonataflowplatform_controller_test.go @@ -23,6 +23,10 @@ import ( "context" "testing" + "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/platform/services" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles/common/constants" + "github.com/apache/incubator-kie-kogito-serverless-operator/test" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -30,12 +34,6 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/platform/services" - "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles/common/constants" - "github.com/apache/incubator-kie-kogito-serverless-operator/test" - - "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" ) var ( @@ -79,7 +77,7 @@ func TestSonataFlowPlatformController(t *testing.T) { assert.Equal(t, "quay.io/kiegroup", ksp.Spec.Build.Config.Registry.Address) assert.Equal(t, "regcred", ksp.Spec.Build.Config.Registry.Secret) assert.Equal(t, v1alpha08.OperatorBuildStrategy, ksp.Spec.Build.Config.BuildStrategy) - assert.Nil(t, ksp.Spec.Services.DataIndex) + assert.Nil(t, ksp.Spec.Services) assert.Equal(t, v1alpha08.PlatformClusterKubernetes, ksp.Status.Cluster) assert.Equal(t, v1alpha08.PlatformCreatingReason, ksp.Status.GetTopLevelCondition().Reason) @@ -89,7 +87,7 @@ func TestSonataFlowPlatformController(t *testing.T) { namespace := t.Name() // Create a SonataFlowPlatform object with metadata and spec. ksp := test.GetBasePlatformInReadyPhase(namespace) - ksp.Spec.Services = v1alpha08.ServicesPlatformSpec{ + ksp.Spec.Services = &v1alpha08.ServicesPlatformSpec{ DataIndex: &v1alpha08.ServiceSpec{}, } @@ -168,7 +166,7 @@ func TestSonataFlowPlatformController(t *testing.T) { // Create a SonataFlowPlatform object with metadata and spec. ksp := test.GetBasePlatformInReadyPhase(namespace) var replicas int32 = 2 - ksp.Spec.Services = v1alpha08.ServicesPlatformSpec{ + ksp.Spec.Services = &v1alpha08.ServicesPlatformSpec{ DataIndex: &v1alpha08.ServiceSpec{ PodTemplate: v1alpha08.PodTemplateSpec{ Replicas: &replicas, @@ -257,7 +255,7 @@ func TestSonataFlowPlatformController(t *testing.T) { namespace := t.Name() // Create a SonataFlowPlatform object with metadata and spec. ksp := test.GetBasePlatformInReadyPhase(namespace) - ksp.Spec.Services = v1alpha08.ServicesPlatformSpec{ + ksp.Spec.Services = &v1alpha08.ServicesPlatformSpec{ JobService: &v1alpha08.ServiceSpec{}, } @@ -334,7 +332,7 @@ func TestSonataFlowPlatformController(t *testing.T) { // Create a SonataFlowPlatform object with metadata and spec. ksp := test.GetBasePlatformInReadyPhase(namespace) var replicas int32 = 2 - ksp.Spec.Services = v1alpha08.ServicesPlatformSpec{ + ksp.Spec.Services = &v1alpha08.ServicesPlatformSpec{ JobService: &v1alpha08.ServiceSpec{ PodTemplate: v1alpha08.PodTemplateSpec{ Replicas: &replicas, @@ -415,7 +413,7 @@ func TestSonataFlowPlatformController(t *testing.T) { namespace := t.Name() // Create a SonataFlowPlatform object with metadata and spec. ksp := test.GetBasePlatformInReadyPhase(namespace) - ksp.Spec.Services = v1alpha08.ServicesPlatformSpec{ + ksp.Spec.Services = &v1alpha08.ServicesPlatformSpec{ DataIndex: &v1alpha08.ServiceSpec{}, JobService: &v1alpha08.ServiceSpec{}, } @@ -475,4 +473,125 @@ func TestSonataFlowPlatformController(t *testing.T) { assert.NotContains(t, dep.Spec.Template.Spec.Containers[0].Env, envDataIndex) }) + t.Run("verify that a basic reconcile of a cluster platform is performed without error", func(t *testing.T) { + namespace := t.Name() + + // Create a SonataFlowClusterPlatform object with metadata and spec. + kscp := test.GetBaseClusterPlatformInReadyPhase(namespace) + + // Create a SonataFlowPlatform object with metadata and spec. + ksp := test.GetBasePlatformInReadyPhase(namespace) + ksp.Spec.Services = &v1alpha08.ServicesPlatformSpec{ + DataIndex: &v1alpha08.ServiceSpec{}, + JobService: &v1alpha08.ServiceSpec{}, + } + ksp2 := test.GetBasePlatformInReadyPhase(namespace) + ksp2.Name = "ksp2" + + // Create a fake client to mock API calls. + cl := test.NewSonataFlowClientBuilder().WithRuntimeObjects(kscp, ksp, ksp2).WithStatusSubresource(kscp, ksp, ksp2).Build() + + // Create a SonataFlowPlatformReconciler object with the scheme and fake client. + r := &SonataFlowPlatformReconciler{cl, cl, cl.Scheme(), &rest.Config{}, &record.FakeRecorder{}} + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: ksp.Name, + Namespace: ksp.Namespace, + }, + } + _, err := r.Reconcile(context.TODO(), req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + + assert.NoError(t, cl.Get(context.TODO(), types.NamespacedName{Name: ksp.Name, Namespace: ksp.Namespace}, ksp)) + assert.Greater(t, len(ksp2.Status.Conditions), 0) + assert.Nil(t, ksp2.Status.ClusterPlatformRef) + + // Create a SonataFlowClusterPlatformReconciler object with the scheme and fake client. + cr := &SonataFlowClusterPlatformReconciler{cl, cl, cl.Scheme(), &rest.Config{}, &record.FakeRecorder{}} + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + cReq := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: kscp.Name, + }, + } + _, err = cr.Reconcile(context.TODO(), cReq) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + + assert.NoError(t, cl.Get(context.TODO(), types.NamespacedName{Name: kscp.Name}, kscp)) + assert.NoError(t, cl.Get(context.TODO(), types.NamespacedName{Name: ksp.Name, Namespace: ksp.Namespace}, ksp)) + + // Perform some checks on the created CR + assert.True(t, ksp.Status.IsReady()) + assert.True(t, kscp.Status.IsReady()) + assert.Equal(t, "quay.io/kiegroup", ksp.Spec.Build.Config.Registry.Address) + assert.Equal(t, "regcred", ksp.Spec.Build.Config.Registry.Secret) + assert.Equal(t, v1alpha08.OperatorBuildStrategy, ksp.Spec.Build.Config.BuildStrategy) + assert.NotNil(t, ksp.Spec.Services.DataIndex) + assert.NotNil(t, ksp.Spec.Services.DataIndex.Enabled) + assert.Equal(t, true, *ksp.Spec.Services.DataIndex.Enabled) + assert.Equal(t, v1alpha08.PlatformClusterKubernetes, ksp.Status.Cluster) + assert.Equal(t, "", ksp.Status.GetTopLevelCondition().Reason) + assert.Equal(t, kscp.Name, ksp.Status.ClusterPlatformRef.Name) + assert.Equal(t, kscp.Spec.PlatformRef.Name, ksp.Status.ClusterPlatformRef.PlatformRef.Name) + assert.Equal(t, kscp.Spec.PlatformRef.Namespace, ksp.Status.ClusterPlatformRef.PlatformRef.Namespace) + + assert.NotNil(t, ksp.Status.ClusterPlatformRef) + assert.Nil(t, ksp.Status.ClusterPlatformRef.Services) + + req2 := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: ksp2.Name, + Namespace: ksp2.Namespace, + }, + } + _, err = r.Reconcile(context.TODO(), req2) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + + assert.NoError(t, cl.Get(context.TODO(), types.NamespacedName{Name: ksp2.Name, Namespace: ksp2.Namespace}, ksp2)) + assert.True(t, ksp2.Status.IsReady()) + assert.NotNil(t, ksp2.Status.ClusterPlatformRef) + assert.Equal(t, kscp.Name, ksp2.Status.ClusterPlatformRef.Name) + assert.Equal(t, kscp.Spec.PlatformRef.Name, ksp2.Status.ClusterPlatformRef.PlatformRef.Name) + assert.Equal(t, kscp.Spec.PlatformRef.Namespace, ksp2.Status.ClusterPlatformRef.PlatformRef.Namespace) + assert.NotNil(t, ksp2.Status.ClusterPlatformRef.Services) + assert.NotNil(t, ksp2.Status.ClusterPlatformRef.Services.DataIndexRef) + assert.NotEmpty(t, ksp2.Status.ClusterPlatformRef.Services.DataIndexRef.Url) + assert.NotNil(t, ksp2.Status.ClusterPlatformRef.Services.JobServiceRef) + assert.NotEmpty(t, ksp2.Status.ClusterPlatformRef.Services.JobServiceRef.Url) + + psDi := services.NewDataIndexHandler(ksp) + psDi2 := services.NewDataIndexHandler(ksp2) + assert.Equal(t, ksp2.Status.ClusterPlatformRef.Services.DataIndexRef.Url, psDi.GetLocalServiceBaseUrl()) + assert.Equal(t, psDi.GetLocalServiceBaseUrl()+constants.KogitoProcessInstancesEventsPath, psDi2.GetServiceUrl()) + psJs := services.NewJobServiceHandler(ksp) + psJs2 := services.NewJobServiceHandler(ksp2) + assert.Equal(t, ksp2.Status.ClusterPlatformRef.Services.JobServiceRef.Url, psJs.GetLocalServiceBaseUrl()) + assert.Equal(t, psJs.GetLocalServiceBaseUrl()+constants.JobServiceURLPath, psJs2.GetServiceUrl()) + + ksp2.Spec.Services = &v1alpha08.ServicesPlatformSpec{} + + assert.NoError(t, cl.Update(context.TODO(), ksp2)) + _, err = r.Reconcile(context.TODO(), req2) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + + assert.NoError(t, cl.Get(context.TODO(), types.NamespacedName{Name: ksp2.Name, Namespace: ksp2.Namespace}, ksp2)) + assert.True(t, ksp2.Status.IsReady()) + assert.NotNil(t, ksp2.Status.ClusterPlatformRef) + assert.Equal(t, kscp.Spec.PlatformRef.Name, ksp2.Status.ClusterPlatformRef.PlatformRef.Name) + assert.Equal(t, kscp.Spec.PlatformRef.Namespace, ksp2.Status.ClusterPlatformRef.PlatformRef.Namespace) + assert.Nil(t, ksp2.Status.ClusterPlatformRef.Services) + }) } diff --git a/main.go b/main.go index 645cfb880..a1040f7bf 100644 --- a/main.go +++ b/main.go @@ -115,6 +115,16 @@ func main() { klog.V(log.E).ErrorS(err, "unable to create controller", "controller", "SonataFlowPlatform") os.Exit(1) } + if err = (&controllers.SonataFlowClusterPlatformReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Reader: mgr.GetAPIReader(), + Config: mgr.GetConfig(), + Recorder: mgr.GetEventRecorderFor("cluster-platform-controller"), + }).SetupWithManager(mgr); err != nil { + klog.V(log.E).ErrorS(err, "unable to create controller", "controller", "SonataFlowClusterPlatform") + os.Exit(1) + } //+kubebuilder:scaffold:builder if utils.IsOpenShift() { diff --git a/operator.yaml b/operator.yaml index 973b5cacc..a016cf252 100644 --- a/operator.yaml +++ b/operator.yaml @@ -364,6 +364,121 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.2 + creationTimestamp: null + name: sonataflowclusterplatforms.sonataflow.org +spec: + group: sonataflow.org + names: + kind: SonataFlowClusterPlatform + listKind: SonataFlowClusterPlatformList + plural: sonataflowclusterplatforms + singular: sonataflowclusterplatform + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.platformRef.name + name: Platform_Name + type: string + - jsonPath: .spec.platformRef.namespace + name: Platform_NS + type: string + - jsonPath: .status.conditions[?(@.type=='Succeed')].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=='Succeed')].reason + name: Reason + type: string + name: v1alpha08 + schema: + openAPIV3Schema: + description: SonataFlowClusterPlatform is the Schema for the sonataflowclusterplatforms + API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SonataFlowClusterPlatformSpec defines the desired state of + SonataFlowClusterPlatform + properties: + platformRef: + description: SonataFlowPlatformRef defines which existing SonataFlowPlatform's + supporting services should be used cluster-wide. + properties: + name: + description: Name of the SonataFlowPlatform + type: string + namespace: + description: Namespace of the SonataFlowPlatform + type: string + required: + - name + - namespace + type: object + required: + - platformRef + type: object + status: + description: SonataFlowClusterPlatformStatus defines the observed state + of SonataFlowClusterPlatform + properties: + conditions: + description: The latest available observations of a resource's current + state. + items: + description: Condition describes the common structure for conditions + in our types + properties: + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human-readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type condition for the given object + type: string + required: + - status + - type + type: object + type: array + observedGeneration: + description: The generation observed by the deployment controller. + format: int64 + type: integer + version: + description: Version the operator version controlling this ClusterPlatform + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.9.2 @@ -786,17 +901,18 @@ spec: type: object services: description: 'Services attributes for deploying supporting applications - like Data Index. Only workflows with the proper annotation will - be configured to use these service(s). `sonataflow.org/profile: - prod`' + like Data Index & Job Service. Only workflows without the `sonataflow.org/profile: + dev` annotation will be configured to use these service(s). Setting + this will override the use of any cluster-scoped services that might + be defined via `SonataFlowClusterPlatform`.' properties: dataIndex: - description: Deploys the Data Index service for use by "prod" - profile workflows. + description: 'Deploys the Data Index service for use by workflows + without the `sonataflow.org/profile: dev` annotation.' properties: enabled: - description: Determines whether "prod" profile workflows should - be configured to use this service + description: 'Determines whether workflows without the `sonataflow.org/profile: + dev` annotation should be configured to use this service' type: boolean persistence: description: Persists service to a datasource of choice. Ephemeral @@ -8663,12 +8779,12 @@ spec: type: object type: object jobService: - description: Deploys the Job service for use by "prod" profile - workflows. + description: 'Deploys the Job service for use by workflows without + the `sonataflow.org/profile: dev` annotation.' properties: enabled: - description: Determines whether "prod" profile workflows should - be configured to use this service + description: 'Determines whether workflows without the `sonataflow.org/profile: + dev` annotation should be configured to use this service' type: boolean persistence: description: Persists service to a datasource of choice. Ephemeral @@ -16546,6 +16662,51 @@ spec: - kubernetes - openshift type: string + clusterPlatformRef: + description: ClusterPlatformRef information related to the (optional) + active SonataFlowClusterPlatform + properties: + name: + description: Name of the active SonataFlowClusterPlatform + type: string + platformRef: + description: PlatformRef displays which SonataFlowPlatform has + been referenced by the active SonataFlowClusterPlatform + properties: + name: + description: Name of the SonataFlowPlatform + type: string + namespace: + description: Namespace of the SonataFlowPlatform + type: string + required: + - name + - namespace + type: object + services: + description: Services displays which cluster-wide services are + being used by this SonataFlowPlatform + properties: + dataIndexRef: + description: DataIndexRef displays information on the cluster-wide + Data Index service + properties: + url: + description: Url displays the base url of a cluster-wide + service + type: string + type: object + jobServiceRef: + description: JobServiceRef displays information on the cluster-wide + Job Service + properties: + url: + description: Url displays the base url of a cluster-wide + service + type: string + type: object + type: object + type: object conditions: description: The latest available observations of a resource's current state. @@ -26175,6 +26336,32 @@ rules: - get - patch - update +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/finalizers + verbs: + - update +- apiGroups: + - sonataflow.org + resources: + - sonataflowclusterplatforms/status + verbs: + - get + - patch + - update - apiGroups: - sonataflow.org resources: diff --git a/test/testdata/sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml b/test/testdata/sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml new file mode 100644 index 000000000..9759c15ae --- /dev/null +++ b/test/testdata/sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml @@ -0,0 +1,25 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +apiVersion: sonataflow.org/v1alpha08 +kind: SonataFlowClusterPlatform +metadata: + name: cluster +spec: + platformRef: + name: sonataflow-platform + namespace: test-ns \ No newline at end of file diff --git a/test/yaml.go b/test/yaml.go index 37d28220d..d1c4006b9 100644 --- a/test/yaml.go +++ b/test/yaml.go @@ -27,19 +27,15 @@ import ( "runtime" "strings" - "github.com/davecgh/go-spew/spew" - "k8s.io/klog/v2" - "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/apache/incubator-kie-kogito-serverless-operator/api" - + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + "github.com/apache/incubator-kie-kogito-serverless-operator/log" + "github.com/davecgh/go-spew/spew" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/yaml" - - "github.com/apache/incubator-kie-kogito-serverless-operator/log" - - operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -53,6 +49,7 @@ const ( sonataFlowPlatformYamlCR = "sonataflow.org_v1alpha08_sonataflowplatform.yaml" sonataFlowPlatformWithCacheMinikubeYamlCR = "sonataflow.org_v1alpha08_sonataflowplatform_withCache_minikube.yaml" sonataFlowPlatformForOpenshift = "sonataflow.org_v1alpha08_sonataflowplatform_openshift.yaml" + sonataFlowClusterPlatformYamlCR = "sonataflow.org_v1alpha08_sonataflowclusterplatform.yaml" sonataFlowBuilderConfig = "sonataflow-operator-builder-config_v1_configmap.yaml" sonataFlowBuildSucceed = "sonataflow.org_v1alpha08_sonataflowbuild.yaml" @@ -86,6 +83,31 @@ func GetKubernetesResource(testFile string, resource client.Object) { } } +func getSonataFlowClusterPlatform(testFile string) *operatorapi.SonataFlowClusterPlatform { + kscp := &operatorapi.SonataFlowClusterPlatform{} + yamlFile, err := os.ReadFile(path.Join(getTestDataDir(), testFile)) + if err != nil { + klog.V(log.E).ErrorS(err, "yamlFile.Get") + panic(err) + } + // Important: Here we are reading the CR deployment file from a given path and creating a &operatorapi.SonataFlowPlatform struct + err = yaml.NewYAMLOrJSONDecoder(bytes.NewReader(yamlFile), 100).Decode(kscp) + if err != nil { + klog.V(log.E).ErrorS(err, "Unmarshal") + panic(err) + } + klog.V(log.D).InfoS("Successfully read KSCP", "kscp", kscp) + kscp.Status.Manager().InitializeConditions() + return kscp +} + +func GetSonataFlowClusterPlatformInReadyPhase(path string, namespace string) *operatorapi.SonataFlowClusterPlatform { + kscp := getSonataFlowClusterPlatform(path) + kscp.Spec.PlatformRef.Namespace = namespace + kscp.Status.Manager().MarkTrue(api.SucceedConditionType) + return kscp +} + func getSonataFlowPlatform(testFile string) *operatorapi.SonataFlowPlatform { ksp := &operatorapi.SonataFlowPlatform{} yamlFile, err := os.ReadFile(path.Join(getTestDataDir(), testFile)) @@ -193,6 +215,10 @@ func GetBaseSonataFlowWithProdOpsProfile(namespace string) *operatorapi.SonataFl return NewSonataFlow(SonataFlowSimpleOpsYamlCR, namespace) } +func GetBaseClusterPlatformInReadyPhase(namespace string) *operatorapi.SonataFlowClusterPlatform { + return GetSonataFlowClusterPlatformInReadyPhase(sonataFlowClusterPlatformYamlCR, namespace) +} + func GetBasePlatformInReadyPhase(namespace string) *operatorapi.SonataFlowPlatform { return GetSonataFlowPlatformInReadyPhase(sonataFlowPlatformYamlCR, namespace) } @@ -213,6 +239,10 @@ func GetBasePlatformWithDevBaseImageInReadyPhase(namespace string) *operatorapi. return platform } +func GetBaseClusterPlatform() *operatorapi.SonataFlowClusterPlatform { + return getSonataFlowClusterPlatform(sonataFlowClusterPlatformYamlCR) +} + func GetBasePlatform() *operatorapi.SonataFlowPlatform { return getSonataFlowPlatform(sonataFlowPlatformYamlCR) } From 5151feeb718dcd2a2e056ffb993f5f64350ee35f Mon Sep 17 00:00:00 2001 From: Walter Medvedeo Date: Fri, 2 Feb 2024 09:10:51 +0100 Subject: [PATCH 3/5] kie-kogito-serverless-operator-361: Add data-index and job service startupProbes to the workflow Deployment (#377) --- controllers/platform/services/properties.go | 6 + controllers/platform/services/services.go | 17 +- .../common/constants/platform_services.go | 13 +- .../common/properties/application_test.go | 15 +- .../sonataflowplatform_controller_test.go | 4 +- controllers/workflowdef/utils.go | 113 +++++++ controllers/workflowdef/utils_suite_test.go | 32 ++ controllers/workflowdef/utils_test.go | 285 ++++++++++++++++++ 8 files changed, 465 insertions(+), 20 deletions(-) create mode 100644 controllers/workflowdef/utils.go create mode 100644 controllers/workflowdef/utils_suite_test.go create mode 100644 controllers/workflowdef/utils_test.go diff --git a/controllers/platform/services/properties.go b/controllers/platform/services/properties.go index dbe6dc3f8..c541f5b04 100644 --- a/controllers/platform/services/properties.go +++ b/controllers/platform/services/properties.go @@ -24,6 +24,8 @@ import ( "net/url" "strings" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/workflowdef" + "github.com/apache/incubator-kie-kogito-serverless-operator/log" "github.com/apache/incubator-kie-kogito-serverless-operator/utils" "k8s.io/klog/v2" @@ -164,6 +166,7 @@ func GenerateDataIndexWorkflowProperties(workflow *operatorapi.SonataFlow, platf if workflow != nil && !profiles.IsDevProfile(workflow) && di.IsServiceEnabled() { props.Set(constants.KogitoProcessDefinitionsEventsEnabled, "true") props.Set(constants.KogitoProcessInstancesEventsEnabled, "true") + props.Set(constants.KogitoDataIndexHealthCheckEnabled, "true") di := NewDataIndexHandler(platform) p, err := di.GenerateWorkflowProperties() if err != nil { @@ -186,6 +189,9 @@ func GenerateJobServiceWorkflowProperties(workflow *operatorapi.SonataFlow, plat props.Set(constants.JobServiceRequestEventsURL, fmt.Sprintf("%s://localhost/v2/jobs/events", constants.JobServiceURLProtocol)) js := NewJobServiceHandler(platform) if workflow != nil && !profiles.IsDevProfile(workflow) && js.IsServiceEnabled() { + if workflowdef.HasTimeouts(workflow) { + props.Set(constants.KogitoJobServiceHealthCheckEnabled, "true") + } p, err := js.GenerateWorkflowProperties() if err != nil { return nil, err diff --git a/controllers/platform/services/services.go b/controllers/platform/services/services.go index e49679e5b..05aedbb60 100644 --- a/controllers/platform/services/services.go +++ b/controllers/platform/services/services.go @@ -81,8 +81,6 @@ type PlatformServiceHandler interface { GetLocalServiceBaseUrl() string // GetServiceBaseUrl returns the base url of the service, based on whether using local or cluster-scoped service. GetServiceBaseUrl() string - // GetServiceUrl returns the service url, based on whether using local or cluster-scoped service. - GetServiceUrl() string // IsServiceEnabled returns true if the service is enabled in either the spec or the status.clusterPlatformRef. IsServiceEnabled() bool // SetServiceUrlInStatus sets the service url in status. if reconciled instance does not have service set in spec AND @@ -150,10 +148,6 @@ func (d DataIndexHandler) IsServiceEnabled() bool { return d.IsServiceEnabledInSpec() || d.isServiceEnabledInStatus() } -func (d DataIndexHandler) GetServiceUrl() string { - return d.GetServiceBaseUrl() + constants.KogitoProcessInstancesEventsPath -} - func (d DataIndexHandler) GetServiceBaseUrl() string { if d.IsServiceEnabledInSpec() { return d.GetLocalServiceBaseUrl() @@ -236,8 +230,9 @@ func (d DataIndexHandler) GetServiceCmName() string { func (d DataIndexHandler) GenerateWorkflowProperties() (*properties.Properties, error) { props := properties.NewProperties() if d.IsServiceEnabled() { + props.Set(constants.KogitoDataIndexURL, d.GetServiceBaseUrl()) props.Set(constants.KogitoProcessDefinitionsEventsURL, d.GetServiceBaseUrl()+constants.KogitoProcessDefinitionsEventsPath) - props.Set(constants.KogitoProcessInstancesEventsURL, d.GetServiceUrl()) + props.Set(constants.KogitoProcessInstancesEventsURL, d.GetServiceBaseUrl()+constants.KogitoProcessInstancesEventsPath) } return props, nil } @@ -313,10 +308,6 @@ func (j JobServiceHandler) IsServiceEnabled() bool { return j.IsServiceEnabledInSpec() || j.isServiceEnabledInStatus() } -func (j JobServiceHandler) GetServiceUrl() string { - return j.GetServiceBaseUrl() + constants.JobServiceURLPath -} - func (j JobServiceHandler) GetServiceBaseUrl() string { if j.IsServiceEnabledInSpec() { return j.GetLocalServiceBaseUrl() @@ -411,8 +402,8 @@ func (j JobServiceHandler) GenerateServiceProperties() (*properties.Properties, func (j JobServiceHandler) GenerateWorkflowProperties() (*properties.Properties, error) { props := properties.NewProperties() if j.IsServiceEnabled() { - // add data source reactive URL - props.Set(constants.JobServiceRequestEventsURL, j.GetServiceUrl()) + props.Set(constants.KogitoJobServiceURL, j.GetServiceBaseUrl()) + props.Set(constants.JobServiceRequestEventsURL, j.GetServiceBaseUrl()+constants.JobServiceJobEventsPath) } return props, nil } diff --git a/controllers/profiles/common/constants/platform_services.go b/controllers/profiles/common/constants/platform_services.go index 3db52e16c..e0f249273 100644 --- a/controllers/profiles/common/constants/platform_services.go +++ b/controllers/profiles/common/constants/platform_services.go @@ -32,7 +32,7 @@ const ( JobServiceStatusChangeEventsURL = "mp.messaging.outgoing.kogito-job-service-job-status-events-http.url" JobServiceURLProtocol = "http" JobServiceDataSourceReactiveURL = "quarkus.datasource.reactive.url" - JobServiceURLPath = "/v2/jobs/events" + JobServiceJobEventsPath = "/v2/jobs/events" KogitoProcessEventsProtocol = "http" KogitoProcessInstancesEventsURL = "mp.messaging.outgoing.kogito-processinstances-events.url" @@ -42,7 +42,16 @@ const ( KogitoProcessDefinitionsEventsEnabled = "kogito.events.processdefinitions.enabled" KogitoProcessDefinitionsEventsPath = "/definitions" KogitoUserTasksEventsEnabled = "kogito.events.usertasks.enabled" - KogitoEventsVariablesEnabled = "kogito.events.variables.enabled" + // KogitoDataIndexHealthCheckEnabled configures if a workflow must check for the data index availability as part + // of its start health check. + KogitoDataIndexHealthCheckEnabled = "kogito.data-index.health-enabled" + // KogitoDataIndexURL configures the data index url, this value can be used internally by the workflow. + KogitoDataIndexURL = "kogito.data-index.url" + // KogitoJobServiceHealthCheckEnabled configures if a workflow must check for the job service availability as part + // of its start health check. + KogitoJobServiceHealthCheckEnabled = "kogito.jobs-service.health-enabled" + // KogitoJobServiceURL configures the jobs service, this value can be used internally by the workflow. + KogitoJobServiceURL = "kogito.jobs-service.url" KogitoServiceURLProperty = "kogito.service.url" KogitoServiceURLProtocol = "http" DataIndexKafkaSmallRyeHealthProperty = `quarkus.smallrye-health.check."io.quarkus.kafka.client.health.KafkaHealthCheck".enabled` diff --git a/controllers/profiles/common/properties/application_test.go b/controllers/profiles/common/properties/application_test.go index 2a95e9674..543247dd7 100644 --- a/controllers/profiles/common/properties/application_test.go +++ b/controllers/profiles/common/properties/application_test.go @@ -239,7 +239,7 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr = properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 15, len(generatedProps.Keys())) + assert.Equal(t, 18, len(generatedProps.Keys())) assert.Equal(t, "http://"+platform.Name+"-"+constants.DataIndexServiceName+"."+platform.Namespace+"/definitions", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsURL, "")) assert.Equal(t, "true", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsEnabled, "")) assert.Equal(t, "http://"+platform.Name+"-"+constants.DataIndexServiceName+"."+platform.Namespace+"/processes", generatedProps.GetString(constants.KogitoProcessInstancesEventsURL, "")) @@ -249,6 +249,9 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.Equal(t, "", generatedProps.GetString(constants.JobServiceDataSourceReactiveURL, "")) assert.Equal(t, "", generatedProps.GetString(constants.JobServiceStatusChangeEvents, "")) assert.Equal(t, "", generatedProps.GetString(constants.JobServiceStatusChangeEventsURL, "")) + assert.Equal(t, "true", generatedProps.GetString(constants.KogitoDataIndexHealthCheckEnabled, "")) + assert.Equal(t, "http://"+platform.Name+"-"+constants.DataIndexServiceName+"."+platform.Namespace, generatedProps.GetString(constants.KogitoDataIndexURL, "")) + assert.Equal(t, "http://"+platform.Name+"-"+constants.JobServiceName+"."+platform.Namespace, generatedProps.GetString(constants.KogitoJobServiceURL, "")) // disabling data index bypasses config of outgoing events url platform.Spec.Services.DataIndex.Enabled = nil @@ -256,7 +259,7 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr = properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 13, len(generatedProps.Keys())) + assert.Equal(t, 14, len(generatedProps.Keys())) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsURL, "")) assert.Equal(t, "false", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsEnabled, "")) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessInstancesEventsURL, "")) @@ -265,6 +268,7 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.Equal(t, "http://"+platform.Name+"-"+constants.JobServiceName+"."+platform.Namespace+"/v2/jobs/events", generatedProps.GetString(constants.JobServiceRequestEventsURL, "")) assert.Equal(t, "", generatedProps.GetString(constants.JobServiceStatusChangeEvents, "")) assert.Equal(t, "", generatedProps.GetString(constants.JobServiceStatusChangeEventsURL, "")) + assert.Equal(t, "http://"+platform.Name+"-"+constants.JobServiceName+"."+platform.Namespace, generatedProps.GetString(constants.KogitoJobServiceURL, "")) // disabling job service bypasses config of outgoing events url platform.Spec.Services.JobService.Enabled = nil @@ -465,6 +469,7 @@ func generateJobServiceWorkflowProductionProperties() *properties.Properties { if jobServiceProdProperties == nil { jobServiceProdProperties = properties.NewProperties() jobServiceProdProperties.Set("kogito.service.url", "http://foo.default") + jobServiceProdProperties.Set("kogito.jobs-service.url", "http://foo-jobs-service.default") jobServiceProdProperties.Set("quarkus.http.host", "0.0.0.0") jobServiceProdProperties.Set("quarkus.http.port", "8080") jobServiceProdProperties.Set("quarkus.kogito.devservices.enabled", "false") @@ -489,7 +494,6 @@ func generateDataIndexWorkflowDevProperties() *properties.Properties { dataIndexDevProperties.Set("quarkus.devservices.enabled", "false") dataIndexDevProperties.Set("quarkus.kogito.devservices.enabled", "false") dataIndexDevProperties.Set("org.kie.kogito.addons.knative.eventing.health-enabled", "false") - //TODO revisar, pero para el dev profile esto no va dataIndexDevProperties.Set("mp.messaging.outgoing.kogito-job-service-job-request-events.connector", "quarkus-http") dataIndexDevProperties.Set("mp.messaging.outgoing.kogito-job-service-job-request-events.url", "http://localhost/v2/jobs/events") dataIndexDevProperties.Set("kogito.events.processdefinitions.enabled", "false") @@ -504,6 +508,8 @@ func generateDataIndexWorkflowProductionProperties() *properties.Properties { if dataIndexProdProperties == nil { dataIndexProdProperties = properties.NewProperties() dataIndexProdProperties.Set("kogito.service.url", "http://foo.default") + dataIndexProdProperties.Set("kogito.data-index.url", "http://foo-data-index-service.default") + dataIndexProdProperties.Set("kogito.data-index.health-enabled", "true") dataIndexProdProperties.Set("quarkus.http.host", "0.0.0.0") dataIndexProdProperties.Set("quarkus.http.port", "8080") dataIndexProdProperties.Set("quarkus.devservices.enabled", "false") @@ -544,6 +550,9 @@ func generateDataIndexAndJobServiceWorkflowProductionProperties() *properties.Pr if dataIndexJobServiceProdProperties == nil { dataIndexJobServiceProdProperties = properties.NewProperties() dataIndexJobServiceProdProperties.Set("kogito.service.url", "http://foo.default") + dataIndexJobServiceProdProperties.Set("kogito.data-index.url", "http://foo-data-index-service.default") + dataIndexJobServiceProdProperties.Set("kogito.data-index.health-enabled", "true") + dataIndexJobServiceProdProperties.Set("kogito.jobs-service.url", "http://foo-jobs-service.default") dataIndexJobServiceProdProperties.Set("quarkus.http.host", "0.0.0.0") dataIndexJobServiceProdProperties.Set("quarkus.http.port", "8080") dataIndexJobServiceProdProperties.Set("quarkus.kogito.devservices.enabled", "false") diff --git a/controllers/sonataflowplatform_controller_test.go b/controllers/sonataflowplatform_controller_test.go index 5594d6449..11e5bb932 100644 --- a/controllers/sonataflowplatform_controller_test.go +++ b/controllers/sonataflowplatform_controller_test.go @@ -573,11 +573,11 @@ func TestSonataFlowPlatformController(t *testing.T) { psDi := services.NewDataIndexHandler(ksp) psDi2 := services.NewDataIndexHandler(ksp2) assert.Equal(t, ksp2.Status.ClusterPlatformRef.Services.DataIndexRef.Url, psDi.GetLocalServiceBaseUrl()) - assert.Equal(t, psDi.GetLocalServiceBaseUrl()+constants.KogitoProcessInstancesEventsPath, psDi2.GetServiceUrl()) + assert.Equal(t, psDi.GetLocalServiceBaseUrl()+constants.KogitoProcessInstancesEventsPath, psDi2.GetServiceBaseUrl()+constants.KogitoProcessInstancesEventsPath) psJs := services.NewJobServiceHandler(ksp) psJs2 := services.NewJobServiceHandler(ksp2) assert.Equal(t, ksp2.Status.ClusterPlatformRef.Services.JobServiceRef.Url, psJs.GetLocalServiceBaseUrl()) - assert.Equal(t, psJs.GetLocalServiceBaseUrl()+constants.JobServiceURLPath, psJs2.GetServiceUrl()) + assert.Equal(t, psJs.GetLocalServiceBaseUrl()+constants.JobServiceJobEventsPath, psJs2.GetServiceBaseUrl()+constants.JobServiceJobEventsPath) ksp2.Spec.Services = &v1alpha08.ServicesPlatformSpec{} diff --git a/controllers/workflowdef/utils.go b/controllers/workflowdef/utils.go new file mode 100644 index 000000000..87a0356f3 --- /dev/null +++ b/controllers/workflowdef/utils.go @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package workflowdef + +import ( + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + "github.com/serverlessworkflow/sdk-go/v2/model" +) + +// HasTimeouts returns true if current workflow has configured any of the SonataFlow supported timeouts, false +// in any other case. This method might be reviewed when more timeouts are supported. +func HasTimeouts(workflow *operatorapi.SonataFlow) bool { + flow := &workflow.Spec.Flow + hasTimeouts := HasWorkflowExecTimeout(flow) || HasWorkflowEventTimeout(flow) + for i := 0; !hasTimeouts && i < len(flow.States); i++ { + state := flow.States[i] + switch state.Type { + case model.StateTypeEvent: + hasTimeouts = HasEventStateTimeouts(state.EventState) + case model.StateTypeOperation: + hasTimeouts = HasOperationStateTimeouts(state.OperationState) + case model.StateTypeSwitch: + hasTimeouts = HasSwitchStateTimeouts(state.SwitchState) + case model.StateTypeSleep: + hasTimeouts = true + case model.StateTypeParallel: + hasTimeouts = HasParallelStateTimeouts(state.ParallelState) + case model.StateTypeForEach: + hasTimeouts = HasForEachStateTimeouts(state.ForEachState) + case model.StateTypeCallback: + hasTimeouts = HasCallbackStateTimeouts(state.CallbackState) + } + } + return hasTimeouts +} + +func HasWorkflowEventTimeout(flow *operatorapi.Flow) bool { + return flow.Timeouts != nil && len(flow.Timeouts.EventTimeout) > 0 +} +func HasWorkflowExecTimeout(flow *operatorapi.Flow) bool { + return flow.Timeouts != nil && flow.Timeouts.WorkflowExecTimeout != nil && len(flow.Timeouts.WorkflowExecTimeout.Duration) > 0 +} + +func HasEventStateTimeouts(state *model.EventState) bool { + if state.Timeouts != nil && len(state.Timeouts.EventTimeout) > 0 { + return true + } + for _, onEvent := range state.OnEvents { + if hasActionsWithSleep(&onEvent.Actions) { + return true + } + } + return false +} + +func HasOperationStateTimeouts(state *model.OperationState) bool { + return hasActionsWithSleep(&state.Actions) +} + +func HasSwitchStateTimeouts(state *model.SwitchState) bool { + return state.Timeouts != nil && len(state.Timeouts.EventTimeout) > 0 +} + +func HasParallelStateTimeouts(state *model.ParallelState) bool { + for _, branch := range state.Branches { + if hasBranchTimeouts(&branch) { + return true + } + } + return false +} + +func hasBranchTimeouts(branch *model.Branch) bool { + return hasActionsWithSleep(&branch.Actions) +} + +func HasForEachStateTimeouts(state *model.ForEachState) bool { + return hasActionsWithSleep(&state.Actions) +} + +func HasCallbackStateTimeouts(state *model.CallbackState) bool { + return (state.Timeouts != nil && len(state.Timeouts.EventTimeout) > 0) || hasAnySleep(&state.Action) +} + +func hasActionsWithSleep(actions *[]model.Action) bool { + for _, action := range *actions { + if hasAnySleep(&action) { + return true + } + } + return false +} + +func hasAnySleep(action *model.Action) bool { + return action.Sleep != nil && (len(action.Sleep.Before) > 0 || len(action.Sleep.After) > 0) +} diff --git a/controllers/workflowdef/utils_suite_test.go b/controllers/workflowdef/utils_suite_test.go new file mode 100644 index 000000000..aa3919a75 --- /dev/null +++ b/controllers/workflowdef/utils_suite_test.go @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package workflowdef + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestProperties(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} diff --git a/controllers/workflowdef/utils_test.go b/controllers/workflowdef/utils_test.go new file mode 100644 index 000000000..9e279b10d --- /dev/null +++ b/controllers/workflowdef/utils_test.go @@ -0,0 +1,285 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package workflowdef + +import ( + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + cncfmodel "github.com/serverlessworkflow/sdk-go/v2/model" +) + +var ( + emptyDuration = "" + isoDuration = "PT30S" +) + +var _ = DescribeTable("Workflow has timeouts", + func(workflow *operatorapi.SonataFlow, expectedHasTimeouts bool) { + hasTimeouts := HasTimeouts(workflow) + Expect(hasTimeouts).Should(Equal(expectedHasTimeouts)) + }, + Entry("for a workflow with WorkflowExecTimeout", workflowWithWorkflowExecTimeout(&isoDuration), true), + Entry("for a workflow with empty WorkflowExecTimeout", workflowWithWorkflowExecTimeout(&emptyDuration), false), + Entry("for a workflow with nil WorkflowExecTimeout", workflowWithWorkflowExecTimeout(&emptyDuration), false), + + Entry("for a workflow with WorkflowEventTimeout", workflowWithWorkflowEventStateTimeout(&isoDuration), true), + Entry("for a workflow with empty WorkflowEventTimeout", workflowWithWorkflowEventStateTimeout(&emptyDuration), false), + Entry("for a workflow with nil WorkflowEventTimeout", workflowWithWorkflowEventStateTimeout(nil), false), + + Entry("for a workflow with EventState with timeouts", workflowWithEventStateWithTimeout(&isoDuration), true), + Entry("for a workflow with EventState empty timeouts", workflowWithEventStateWithTimeout(&emptyDuration), false), + Entry("for a workflow with EventState nil timeouts", workflowWithEventStateWithTimeout(&emptyDuration), false), + Entry("for a workflow with EventState with action sleep at before", workflowWithEventStateWithActionSleep(true, false), true), + Entry("for a workflow with EventState with action sleep at before", workflowWithEventStateWithActionSleep(false, true), true), + + Entry("for a workflow with OperationState with action sleep at before", workflowWithEventStateWithActionSleep(true, false), true), + Entry("for a workflow with OperationState with with action sleep at after", workflowWithEventStateWithActionSleep(false, true), true), + Entry("for a workflow with OperationState with no action sleep", workflowWithEventStateWithActionSleep(false, false), false), + + Entry("for a workflow with SwitchState with timeouts", workflowWithSwitchStateWithTimeout(&isoDuration), true), + Entry("for a workflow with SwitchState with empty timeouts", workflowWithSwitchStateWithTimeout(&emptyDuration), false), + Entry("for a workflow with SwitchState with nil timeouts", workflowWithSwitchStateWithTimeout(nil), false), + + Entry("for a workflow with SleepState", workflowWithSleepState(), true), + + Entry("for a workflow with ParallelState with branch with sleep at before", workflowWithParallelState(true, false), true), + Entry("for a workflow with ParallelState with branch with sleep at after", workflowWithParallelState(false, true), true), + Entry("for a workflow with ParallelState with branches with sleep at before and after", workflowWithParallelState(true, true), true), + Entry("for a workflow with ParallelState with no sleep branches", workflowWithParallelState(false, false), false), + + Entry("for a workflow with ForEachState with action sleep at before", workflowWithForEachStateWithActionSleep(true, false), true), + Entry("for a workflow with ForEachState with with action sleep at after", workflowWithForEachStateWithActionSleep(false, true), true), + Entry("for a workflow with ForEachState with no action sleep", workflowWithForEachStateWithActionSleep(false, false), false), + + Entry("for a workflow with CallbackState with timeouts", workflowWithCallbackStateTimeoutAndActionSleep(&isoDuration, nil, nil), true), + Entry("for a workflow with CallbackState with nil timeouts and before action sleep", workflowWithCallbackStateTimeoutAndActionSleep(nil, &isoDuration, nil), true), + Entry("for a workflow with CallbackState with nil timeouts and after action sleep", workflowWithCallbackStateTimeoutAndActionSleep(nil, nil, &isoDuration), true), + Entry("for a workflow with CallbackState with nil timeouts and no action sleep", workflowWithCallbackStateTimeoutAndActionSleep(nil, nil, nil), false), +) + +func workflowWithWorkflowExecTimeout(duration *string) *operatorapi.SonataFlow { + wf := generateWorkflow() + if duration != nil { + wf.Spec.Flow.Timeouts = &cncfmodel.Timeouts{} + wf.Spec.Flow.Timeouts.WorkflowExecTimeout = &cncfmodel.WorkflowExecTimeout{ + Duration: *duration, + } + } + return wf +} + +func workflowWithWorkflowEventStateTimeout(duration *string) *operatorapi.SonataFlow { + wf := generateWorkflow() + if duration != nil { + wf.Spec.Flow.Timeouts = &cncfmodel.Timeouts{ + EventTimeout: *duration, + } + } + return wf +} + +func workflowWithEventStateWithTimeout(duration *string) *operatorapi.SonataFlow { + wf := generateWorkflow() + state := generateEventState() + if duration != nil { + state.EventState.Timeouts = &cncfmodel.EventStateTimeout{EventTimeout: *duration} + } + wf.Spec.Flow.States = []cncfmodel.State{*state} + return wf +} + +func workflowWithEventStateWithActionSleep(before bool, after bool) *operatorapi.SonataFlow { + wf := generateWorkflow() + state := generateEventState() + wf.Spec.Flow.States = []cncfmodel.State{*state} + state.EventState.OnEvents = []cncfmodel.OnEvents{ + { + Actions: generateActionsWithSleep(before, after), + }, + } + return wf +} + +func workflowWithOperationStateWithActionSleep(before bool, after bool) *operatorapi.SonataFlow { + wf := generateWorkflow() + state := generateOperationState() + wf.Spec.Flow.States = []cncfmodel.State{*state} + state.OperationState.Actions = generateActionsWithSleep(before, after) + return wf +} + +func workflowWithSwitchStateWithTimeout(duration *string) *operatorapi.SonataFlow { + wf := generateWorkflow() + state := generateSwitchState() + wf.Spec.Flow.States = []cncfmodel.State{*state} + if duration != nil { + state.SwitchState.Timeouts = &cncfmodel.SwitchStateTimeout{ + EventTimeout: *duration, + } + } + return wf +} + +func workflowWithSleepState() *operatorapi.SonataFlow { + wf := generateWorkflow() + wf.Spec.Flow.States = []cncfmodel.State{*generateSleepState()} + return wf +} + +func workflowWithParallelState(branchWithBeforeSleep bool, branchWithAfterSleep bool) *operatorapi.SonataFlow { + wf := generateWorkflow() + state := generateParallelState() + wf.Spec.Flow.States = []cncfmodel.State{*state} + if branchWithBeforeSleep { + branch := cncfmodel.Branch{ + Actions: []cncfmodel.Action{{Sleep: &cncfmodel.Sleep{Before: "PT5S"}}}, + } + state.ParallelState.Branches = append(state.ParallelState.Branches, branch) + } + if branchWithAfterSleep { + branch := cncfmodel.Branch{ + Actions: []cncfmodel.Action{{Sleep: &cncfmodel.Sleep{After: "PT5S"}}}, + } + state.ParallelState.Branches = append(state.ParallelState.Branches, branch) + } + return wf +} + +func workflowWithForEachStateWithActionSleep(before bool, after bool) *operatorapi.SonataFlow { + wf := generateWorkflow() + state := generateForEachState() + wf.Spec.Flow.States = []cncfmodel.State{*state} + state.ForEachState.Actions = generateActionsWithSleep(before, after) + return wf +} + +func workflowWithCallbackStateTimeoutAndActionSleep(duration *string, before *string, after *string) *operatorapi.SonataFlow { + wf := generateWorkflow() + state := generateCallbackState() + wf.Spec.Flow.States = []cncfmodel.State{*state} + if duration != nil { + state.CallbackState.Timeouts = &cncfmodel.CallbackStateTimeout{EventTimeout: *duration} + } + state.CallbackState.Action = cncfmodel.Action{} + if before != nil || after != nil { + state.CallbackState.Action.Sleep = &cncfmodel.Sleep{} + if before != nil { + state.CallbackState.Action.Sleep.Before = *before + } + if after != nil { + state.CallbackState.Action.Sleep.After = *after + } + } + return wf +} + +func generateWorkflow() *operatorapi.SonataFlow { + wf := &operatorapi.SonataFlow{ + Spec: operatorapi.SonataFlowSpec{ + Flow: operatorapi.Flow{}, + }, + } + return wf +} + +func generateEventState() *cncfmodel.State { + return &cncfmodel.State{ + BaseState: cncfmodel.BaseState{ + Type: cncfmodel.StateTypeEvent, + }, + EventState: &cncfmodel.EventState{}, + } +} + +func generateOperationState() *cncfmodel.State { + return &cncfmodel.State{ + BaseState: cncfmodel.BaseState{ + Type: cncfmodel.StateTypeOperation, + }, + OperationState: &cncfmodel.OperationState{}, + } +} + +func generateSwitchState() *cncfmodel.State { + return &cncfmodel.State{ + BaseState: cncfmodel.BaseState{ + Type: cncfmodel.StateTypeSwitch, + }, + SwitchState: &cncfmodel.SwitchState{}, + } +} + +func generateSleepState() *cncfmodel.State { + return &cncfmodel.State{ + BaseState: cncfmodel.BaseState{ + Type: cncfmodel.StateTypeSleep, + }, + SleepState: &cncfmodel.SleepState{}, + } +} + +func generateParallelState() *cncfmodel.State { + return &cncfmodel.State{ + BaseState: cncfmodel.BaseState{ + Type: cncfmodel.StateTypeParallel, + }, + ParallelState: &cncfmodel.ParallelState{ + Branches: []cncfmodel.Branch{}, + }, + } +} + +func generateForEachState() *cncfmodel.State { + return &cncfmodel.State{ + BaseState: cncfmodel.BaseState{ + Type: cncfmodel.StateTypeForEach, + }, + ForEachState: &cncfmodel.ForEachState{}, + } +} + +func generateCallbackState() *cncfmodel.State { + return &cncfmodel.State{ + BaseState: cncfmodel.BaseState{ + Type: cncfmodel.StateTypeCallback, + }, + CallbackState: &cncfmodel.CallbackState{}, + } +} + +func generateActionsWithSleep(before bool, after bool) []cncfmodel.Action { + var actions []cncfmodel.Action + if before { + actions = append(actions, cncfmodel.Action{ + Sleep: &cncfmodel.Sleep{ + Before: "PT30S", + }, + }) + } + if after { + actions = append(actions, cncfmodel.Action{ + Sleep: &cncfmodel.Sleep{ + After: "PT30S", + }, + }) + } + return actions +} From f40250e6fa4cace36e0219c5444a81e9f41d1fc9 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini <1538000+ricardozanini@users.noreply.github.com> Date: Fri, 2 Feb 2024 09:24:46 -0300 Subject: [PATCH 4/5] Fix #366 - Add custom label to manifests to avoid clashing with installed operators in a cluster (#373) Signed-off-by: Ricardo Zanini --- .github/workflows/e2e.yml | 2 +- ...ontroller-manager-metrics-service_v1_service.yaml | 4 ++-- .../logic-operator-rhel8.clusterserviceversion.yaml | 6 +++--- ...ontroller-manager-metrics-service_v1_service.yaml | 4 ++-- .../sonataflow-operator.clusterserviceversion.yaml | 6 +++--- config/manager/manager.yaml | 8 ++++---- config/manager/osl/manager.yaml | 8 ++++---- config/prometheus/monitor.yaml | 4 ++-- config/rbac/auth_proxy_service.yaml | 4 ++-- operator.yaml | 12 ++++++------ testbdd/steps/operator.go | 2 +- 11 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 26dc484e7..da3202d3f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -70,7 +70,7 @@ jobs: - name: Deploy operator run: | make deploy IMG=${{ env.OPERATOR_IMAGE_NAME }} - kubectl wait pod -A -l control-plane=controller-manager --for condition=Ready + kubectl wait pod -A -l control-plane=sonataflow-operator --for condition=Ready - name: Run tests run: | diff --git a/bundle.osl/manifests/logic-operator-rhel8-controller-manager-metrics-service_v1_service.yaml b/bundle.osl/manifests/logic-operator-rhel8-controller-manager-metrics-service_v1_service.yaml index 43c03ea78..1cbd241cc 100644 --- a/bundle.osl/manifests/logic-operator-rhel8-controller-manager-metrics-service_v1_service.yaml +++ b/bundle.osl/manifests/logic-operator-rhel8-controller-manager-metrics-service_v1_service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: creationTimestamp: null labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: logic-operator-rhel8-controller-manager-metrics-service spec: ports: @@ -12,6 +12,6 @@ spec: protocol: TCP targetPort: https selector: - control-plane: controller-manager + control-plane: sonataflow-operator status: loadBalancer: {} diff --git a/bundle.osl/manifests/logic-operator-rhel8.clusterserviceversion.yaml b/bundle.osl/manifests/logic-operator-rhel8.clusterserviceversion.yaml index 8f0e8abd0..bf035854b 100644 --- a/bundle.osl/manifests/logic-operator-rhel8.clusterserviceversion.yaml +++ b/bundle.osl/manifests/logic-operator-rhel8.clusterserviceversion.yaml @@ -510,20 +510,20 @@ spec: serviceAccountName: logic-operator-rhel8-controller-manager deployments: - label: - control-plane: controller-manager + control-plane: sonataflow-operator name: logic-operator-rhel8-controller-manager spec: replicas: 1 selector: matchLabels: - control-plane: controller-manager + control-plane: sonataflow-operator strategy: {} template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: - control-plane: controller-manager + control-plane: sonataflow-operator spec: containers: - args: diff --git a/bundle/manifests/sonataflow-operator-controller-manager-metrics-service_v1_service.yaml b/bundle/manifests/sonataflow-operator-controller-manager-metrics-service_v1_service.yaml index d9f1354fe..a00cdcf0c 100644 --- a/bundle/manifests/sonataflow-operator-controller-manager-metrics-service_v1_service.yaml +++ b/bundle/manifests/sonataflow-operator-controller-manager-metrics-service_v1_service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: creationTimestamp: null labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: sonataflow-operator-controller-manager-metrics-service spec: ports: @@ -12,6 +12,6 @@ spec: protocol: TCP targetPort: https selector: - control-plane: controller-manager + control-plane: sonataflow-operator status: loadBalancer: {} diff --git a/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml b/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml index 9dced8cd4..65359c86e 100644 --- a/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml +++ b/bundle/manifests/sonataflow-operator.clusterserviceversion.yaml @@ -651,20 +651,20 @@ spec: serviceAccountName: sonataflow-operator-controller-manager deployments: - label: - control-plane: controller-manager + control-plane: sonataflow-operator name: sonataflow-operator-controller-manager spec: replicas: 1 selector: matchLabels: - control-plane: controller-manager + control-plane: sonataflow-operator strategy: {} template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: - control-plane: controller-manager + control-plane: sonataflow-operator spec: containers: - args: diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index f5485cdc6..3a5d65124 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Namespace metadata: labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: system --- apiVersion: apps/v1 @@ -11,18 +11,18 @@ metadata: name: controller-manager namespace: system labels: - control-plane: controller-manager + control-plane: sonataflow-operator spec: selector: matchLabels: - control-plane: controller-manager + control-plane: sonataflow-operator replicas: 1 template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: - control-plane: controller-manager + control-plane: sonataflow-operator spec: securityContext: runAsNonRoot: true diff --git a/config/manager/osl/manager.yaml b/config/manager/osl/manager.yaml index f5485cdc6..3a5d65124 100644 --- a/config/manager/osl/manager.yaml +++ b/config/manager/osl/manager.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Namespace metadata: labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: system --- apiVersion: apps/v1 @@ -11,18 +11,18 @@ metadata: name: controller-manager namespace: system labels: - control-plane: controller-manager + control-plane: sonataflow-operator spec: selector: matchLabels: - control-plane: controller-manager + control-plane: sonataflow-operator replicas: 1 template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: - control-plane: controller-manager + control-plane: sonataflow-operator spec: securityContext: runAsNonRoot: true diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml index d19136ae7..87586daaa 100644 --- a/config/prometheus/monitor.yaml +++ b/config/prometheus/monitor.yaml @@ -4,7 +4,7 @@ apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: controller-manager-metrics-monitor namespace: system spec: @@ -17,4 +17,4 @@ spec: insecureSkipVerify: true selector: matchLabels: - control-plane: controller-manager + control-plane: sonataflow-operator diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml index 71f179727..7e961aa7b 100644 --- a/config/rbac/auth_proxy_service.yaml +++ b/config/rbac/auth_proxy_service.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Service metadata: labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: controller-manager-metrics-service namespace: system spec: @@ -12,4 +12,4 @@ spec: protocol: TCP targetPort: https selector: - control-plane: controller-manager + control-plane: sonataflow-operator diff --git a/operator.yaml b/operator.yaml index a016cf252..c7c538125 100644 --- a/operator.yaml +++ b/operator.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Namespace metadata: labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: sonataflow-operator-system --- apiVersion: apiextensions.k8s.io/v1 @@ -26735,7 +26735,7 @@ apiVersion: v1 kind: Service metadata: labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: sonataflow-operator-controller-manager-metrics-service namespace: sonataflow-operator-system spec: @@ -26745,26 +26745,26 @@ spec: protocol: TCP targetPort: https selector: - control-plane: controller-manager + control-plane: sonataflow-operator --- apiVersion: apps/v1 kind: Deployment metadata: labels: - control-plane: controller-manager + control-plane: sonataflow-operator name: sonataflow-operator-controller-manager namespace: sonataflow-operator-system spec: replicas: 1 selector: matchLabels: - control-plane: controller-manager + control-plane: sonataflow-operator template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: - control-plane: controller-manager + control-plane: sonataflow-operator spec: containers: - args: diff --git a/testbdd/steps/operator.go b/testbdd/steps/operator.go index 8ed78a3b4..17467689c 100644 --- a/testbdd/steps/operator.go +++ b/testbdd/steps/operator.go @@ -53,7 +53,7 @@ func (data *Data) sonataFlowOperatorIsDeployed() (err error) { } //func (data *Data) sonataFlowOperatorHasPodsRunning(numberOfPods int, name, phase string) error { -// return framework.WaitForPodsWithLabel(data.Namespace, "control-plane", "controller-manager", numberOfPods, 1) +// return framework.WaitForPodsWithLabel(data.Namespace, "control-plane", "sonataflow-operator", numberOfPods, 1) //} // //func (data *Data) kogitoOperatorShouldBeInstalled() error { From 2964c1ff4cf9b463b2c01038833ca470e10f9b8f Mon Sep 17 00:00:00 2001 From: Daniele Martinoli <86618610+dmartinol@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:22:39 +0100 Subject: [PATCH 5/5] Break the current operator's configuration into custom and managed properties (#367) * Initial commit * integrating comments: introducing ObjectEnsurerWithPlatform and ObjectCreatorWithPlatform * Removing fewe more unneeded references to platform * reviewed workflowProjectHandler. removed user props from managed props * fixed unit tests * workarond for failed discovery options * fixed broken unit tests * fixed mutator for user props * reviewed hashing function * Anticipating deactivation of broken e2e test * integrating comments: removing unneeded comment * adding discovered value to properties whose value mathes the service discovery pattern * removed unused package common_test * Renamed managed props visitor and reviewed description of NewAppPropertyHandler --- controllers/profiles/common/ensurer.go | 39 +++++++++++++ .../profiles/common/mutate_visitors.go | 32 +++++------ .../profiles/common/object_creators.go | 17 ++++-- .../profiles/common/object_creators_test.go | 57 ++++++++++--------- .../profiles/common/properties/application.go | 33 ++++++----- .../common/properties/application_test.go | 45 +++++++++------ .../profiles/common/properties/discovery.go | 2 + .../common/properties/discovery_test.go | 7 ++- .../profiles/dev/object_creators_dev.go | 5 +- .../profiles/dev/object_creators_dev_test.go | 1 - controllers/profiles/dev/profile_dev.go | 33 ++++++----- controllers/profiles/dev/profile_dev_test.go | 24 +++++--- controllers/profiles/dev/states_dev.go | 10 +++- .../profiles/prod/deployment_handler.go | 26 ++++++--- .../profiles/prod/deployment_handler_test.go | 52 ++++++++++------- .../profiles/prod/object_creators_prod.go | 11 ++-- controllers/profiles/prod/profile_prod.go | 14 +++-- utils/kubernetes/deployment.go | 18 ++++-- workflowproj/operator.go | 50 ++++++++++++---- workflowproj/workflowproj.go | 3 +- workflowproj/workflowproj_test.go | 1 + 21 files changed, 312 insertions(+), 168 deletions(-) diff --git a/controllers/profiles/common/ensurer.go b/controllers/profiles/common/ensurer.go index 0d9cb472e..7d258dbb3 100644 --- a/controllers/profiles/common/ensurer.go +++ b/controllers/profiles/common/ensurer.go @@ -36,6 +36,9 @@ var _ ObjectEnsurer = &noopObjectEnsurer{} type ObjectEnsurer interface { Ensure(ctx context.Context, workflow *operatorapi.SonataFlow, visitors ...MutateVisitor) (client.Object, controllerutil.OperationResult, error) } +type ObjectEnsurerWithPlatform interface { + Ensure(ctx context.Context, workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform, visitors ...MutateVisitor) (client.Object, controllerutil.OperationResult, error) +} // MutateVisitor is a visitor function that mutates the given object before performing any updates in the cluster. // It gets called after the objectEnforcer reference. @@ -56,6 +59,14 @@ func NewObjectEnsurer(client client.Client, creator ObjectCreator) ObjectEnsurer } } +// NewObjectEnsurerWithPlatform see defaultObjectEnsurerWithPLatform +func NewObjectEnsurerWithPlatform(client client.Client, creator ObjectCreatorWithPlatform) ObjectEnsurerWithPlatform { + return &defaultObjectEnsurerWithPlatform{ + c: client, + creator: creator, + } +} + // defaultObjectEnsurer provides the engine for a ReconciliationState that needs to create or update a given Kubernetes object during the reconciliation cycle. type defaultObjectEnsurer struct { c client.Client @@ -84,6 +95,34 @@ func (d *defaultObjectEnsurer) Ensure(ctx context.Context, workflow *operatorapi return object, result, nil } +// defaultObjectEnsurerWithPlatform is the equivalent of defaultObjectEnsurer for resources that require a reference to the SonataFlowPlatform +type defaultObjectEnsurerWithPlatform struct { + c client.Client + creator ObjectCreatorWithPlatform +} + +func (d *defaultObjectEnsurerWithPlatform) Ensure(ctx context.Context, workflow *operatorapi.SonataFlow, pl *operatorapi.SonataFlowPlatform, visitors ...MutateVisitor) (client.Object, controllerutil.OperationResult, error) { + result := controllerutil.OperationResultNone + + object, err := d.creator(workflow, pl) + if err != nil { + return nil, result, err + } + if result, err = controllerutil.CreateOrPatch(ctx, d.c, object, + func() error { + for _, v := range visitors { + if visitorErr := v(object)(); visitorErr != nil { + return visitorErr + } + } + return controllerutil.SetControllerReference(workflow, object, d.c.Scheme()) + }); err != nil { + return nil, result, err + } + klog.V(log.I).InfoS("Object operation finalized", "result", result, "kind", object.GetObjectKind().GroupVersionKind().String(), "name", object.GetName(), "namespace", object.GetNamespace()) + return object, result, nil +} + // NewNoopObjectEnsurer see noopObjectEnsurer func NewNoopObjectEnsurer() ObjectEnsurer { return &noopObjectEnsurer{} diff --git a/controllers/profiles/common/mutate_visitors.go b/controllers/profiles/common/mutate_visitors.go index ffe9784a3..8ccbcab35 100644 --- a/controllers/profiles/common/mutate_visitors.go +++ b/controllers/profiles/common/mutate_visitors.go @@ -105,32 +105,28 @@ func ServiceMutateVisitor(workflow *operatorapi.SonataFlow) MutateVisitor { } } -func WorkflowPropertiesMutateVisitor(ctx context.Context, catalog discovery.ServiceCatalog, - workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform) MutateVisitor { +func ManagedPropertiesMutateVisitor(ctx context.Context, catalog discovery.ServiceCatalog, + workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform, userProps *corev1.ConfigMap) MutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { - if kubeutil.IsObjectNew(object) { - return nil - } - cm := object.(*corev1.ConfigMap) - cm.Labels = workflow.GetLabels() - _, hasKey := cm.Data[workflowproj.ApplicationPropertiesFileName] + managedProps := object.(*corev1.ConfigMap) + managedProps.Labels = workflow.GetLabels() + _, hasKey := managedProps.Data[workflowproj.GetManagedPropertiesFileName(workflow)] if !hasKey { - cm.Data = make(map[string]string, 1) - props, err := properties.ImmutableApplicationProperties(workflow, platform) - if err != nil { - return err - } - cm.Data[workflowproj.ApplicationPropertiesFileName] = props - return nil + managedProps.Data = make(map[string]string, 1) + managedProps.Data[workflowproj.GetManagedPropertiesFileName(workflow)] = "" } + userProperties, hasKey := userProps.Data[workflowproj.ApplicationPropertiesFileName] + if !hasKey { + userProperties = "" + } // In the future, if this needs change, instead we can receive an AppPropertyHandler in this mutator props, err := properties.NewAppPropertyHandler(workflow, platform) if err != nil { return err } - cm.Data[workflowproj.ApplicationPropertiesFileName] = props.WithUserProperties(cm.Data[workflowproj.ApplicationPropertiesFileName]). + managedProps.Data[workflowproj.GetManagedPropertiesFileName(workflow)] = props.WithUserProperties(userProperties). WithServiceDiscovery(ctx, catalog). Build() return nil @@ -142,11 +138,11 @@ func WorkflowPropertiesMutateVisitor(ctx context.Context, catalog discovery.Serv // This method can be used as an alternative to the Kubernetes ConfigMap refresher. // // See: https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically -func RolloutDeploymentIfCMChangedMutateVisitor(cm *v1.ConfigMap) MutateVisitor { +func RolloutDeploymentIfCMChangedMutateVisitor(workflow *operatorapi.SonataFlow, userPropsCM *v1.ConfigMap, managedPropsCM *v1.ConfigMap) MutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { deployment := object.(*appsv1.Deployment) - err := kubeutil.AnnotateDeploymentConfigChecksum(deployment, cm) + err := kubeutil.AnnotateDeploymentConfigChecksum(workflow, deployment, userPropsCM, managedPropsCM) return err } } diff --git a/controllers/profiles/common/object_creators.go b/controllers/profiles/common/object_creators.go index 5739b6ad5..0e2d6f209 100644 --- a/controllers/profiles/common/object_creators.go +++ b/controllers/profiles/common/object_creators.go @@ -41,6 +41,10 @@ import ( // Can be used as a reference to keep the object immutable type ObjectCreator func(workflow *operatorapi.SonataFlow) (client.Object, error) +// ObjectCreatorWithPlatform is the func equivalent to ObjectCreator to use when the resource being created needs a reference to the +// SonataFlowPlatform +type ObjectCreatorWithPlatform func(workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform) (client.Object, error) + const ( defaultHTTPServicePort = 80 @@ -209,13 +213,18 @@ func OpenShiftRouteCreator(workflow *operatorapi.SonataFlow) (client.Object, err return route, err } -// WorkflowPropsConfigMapCreator creates a ConfigMap to hold the external application properties -func WorkflowPropsConfigMapCreator(workflow *operatorapi.SonataFlow) (client.Object, error) { - props, err := properties.ImmutableApplicationProperties(workflow, nil) +// UserPropsConfigMapCreator creates an empty ConfigMap to hold the user application properties +func UserPropsConfigMapCreator(workflow *operatorapi.SonataFlow) (client.Object, error) { + return workflowproj.CreateNewUserPropsConfigMap(workflow), nil +} + +// ManagedPropsConfigMapCreator creates an empty ConfigMap to hold the external application properties +func ManagedPropsConfigMapCreator(workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform) (client.Object, error) { + props, err := properties.ImmutableApplicationProperties(workflow, platform) if err != nil { return nil, err } - return workflowproj.CreateNewAppPropsConfigMap(workflow, props), nil + return workflowproj.CreateNewManagedPropsConfigMap(workflow, props), nil } func ConfigurePersistence(serviceContainer *corev1.Container, options *operatorapi.PersistenceOptions, defaultSchema, namespace string) *corev1.Container { diff --git a/controllers/profiles/common/object_creators_test.go b/controllers/profiles/common/object_creators_test.go index 64e8f43b7..ae252248e 100644 --- a/controllers/profiles/common/object_creators_test.go +++ b/controllers/profiles/common/object_creators_test.go @@ -27,7 +27,6 @@ import ( "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/apache/incubator-kie-kogito-serverless-operator/utils" kubeutil "github.com/apache/incubator-kie-kogito-serverless-operator/utils/kubernetes" @@ -39,50 +38,56 @@ import ( func Test_ensureWorkflowPropertiesConfigMapMutator(t *testing.T) { workflow := test.GetBaseSonataFlowWithDevProfile(t.Name()) + platform := test.GetBasePlatform() // can't be new - cm, _ := WorkflowPropsConfigMapCreator(workflow) - cm.SetUID("1") - cm.SetResourceVersion("1") - reflectCm := cm.(*corev1.ConfigMap) + managedProps, _ := ManagedPropsConfigMapCreator(workflow, platform) + managedProps.SetUID("1") + managedProps.SetResourceVersion("1") + managedPropsCM := managedProps.(*corev1.ConfigMap) - visitor := WorkflowPropertiesMutateVisitor(context.TODO(), nil, workflow, nil) - mutateFn := visitor(cm) + userProps, _ := UserPropsConfigMapCreator(workflow) + userPropsCM := userProps.(*corev1.ConfigMap) + visitor := ManagedPropertiesMutateVisitor(context.TODO(), nil, workflow, nil, userPropsCM) + mutateFn := visitor(managedProps) assert.NoError(t, mutateFn()) - assert.NotEmpty(t, reflectCm.Data[workflowproj.ApplicationPropertiesFileName]) + assert.Empty(t, managedPropsCM.Data[workflowproj.ApplicationPropertiesFileName]) + assert.NotEmpty(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) - props := properties.MustLoadString(reflectCm.Data[workflowproj.ApplicationPropertiesFileName]) + props := properties.MustLoadString(managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) assert.Equal(t, "8080", props.GetString("quarkus.http.port", "")) // we change the properties to something different, we add ours and change the default - reflectCm.Data[workflowproj.ApplicationPropertiesFileName] = "quarkus.http.port=9090\nmy.new.prop=1" - visitor(reflectCm) + userPropsCM.Data[workflowproj.ApplicationPropertiesFileName] = "quarkus.http.port=9090\nmy.new.prop=1" + visitor(managedPropsCM) assert.NoError(t, mutateFn()) // we should preserve the default, and still got ours - props = properties.MustLoadString(reflectCm.Data[workflowproj.ApplicationPropertiesFileName]) + props = properties.MustLoadString(managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) assert.Equal(t, "8080", props.GetString("quarkus.http.port", "")) assert.Equal(t, "0.0.0.0", props.GetString("quarkus.http.host", "")) - assert.Equal(t, "1", props.GetString("my.new.prop", "")) + assert.NotContains(t, "my.new.prop", props.Keys()) } func Test_ensureWorkflowPropertiesConfigMapMutator_DollarReplacement(t *testing.T) { workflow := test.GetBaseSonataFlowWithDevProfile(t.Name()) - existingCM := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: workflow.Name, - Namespace: workflow.Namespace, - UID: "0000-0001-0002-0003", - }, - Data: map[string]string{ - workflowproj.ApplicationPropertiesFileName: "mp.messaging.outgoing.kogito_outgoing_stream.url=${kubernetes:services.v1/event-listener}", - }, - } - mutateVisitorFn := WorkflowPropertiesMutateVisitor(context.TODO(), nil, workflow, nil) + platform := test.GetBasePlatform() + managedProps, _ := ManagedPropsConfigMapCreator(workflow, platform) + managedProps.SetName(workflow.Name) + managedProps.SetNamespace(workflow.Namespace) + managedProps.SetUID("0000-0001-0002-0003") + managedPropsCM := managedProps.(*corev1.ConfigMap) + + userProps, _ := UserPropsConfigMapCreator(workflow) + userPropsCM := userProps.(*corev1.ConfigMap) + userPropsCM.Data[workflowproj.ApplicationPropertiesFileName] = "mp.messaging.outgoing.kogito_outgoing_stream.url=${kubernetes:services.v1/event-listener}" + + mutateVisitorFn := ManagedPropertiesMutateVisitor(context.TODO(), nil, workflow, nil, userPropsCM) - err := mutateVisitorFn(existingCM)() + err := mutateVisitorFn(managedPropsCM)() assert.NoError(t, err) - assert.Contains(t, existingCM.Data[workflowproj.ApplicationPropertiesFileName], "${kubernetes:services.v1/event-listener}") + assert.NotContains(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)], "mp.messaging.outgoing.kogito_outgoing_stream.url") + // assert.Contains(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)], "${kubernetes:services.v1/event-listener}") } func TestMergePodSpec(t *testing.T) { diff --git a/controllers/profiles/common/properties/application.go b/controllers/profiles/common/properties/application.go index fda4ec1e4..414891405 100644 --- a/controllers/profiles/common/properties/application.go +++ b/controllers/profiles/common/properties/application.go @@ -75,36 +75,34 @@ func (a *appPropertyHandler) WithServiceDiscovery(ctx context.Context, catalog d } func (a *appPropertyHandler) Build() string { - var props *properties.Properties + var userProps *properties.Properties var propErr error = nil if len(a.userProperties) == 0 { - props = properties.NewProperties() + userProps = properties.NewProperties() } else { - props, propErr = properties.LoadString(a.userProperties) + userProps, propErr = properties.LoadString(a.userProperties) } if propErr != nil { klog.V(log.D).InfoS("Can't load user's property", "workflow", a.workflow.Name, "namespace", a.workflow.Namespace, "properties", a.userProperties) - props = properties.NewProperties() + userProps = properties.NewProperties() } // Disable expansions since it's not our responsibility // Property expansion means resolving ${} within the properties and environment context. Quarkus will do that in runtime. - props.DisableExpansion = true + userProps.DisableExpansion = true - removeDiscoveryProperties(props) + removeDiscoveryProperties(userProps) + discoveryProps := properties.NewProperties() if a.requireServiceDiscovery() { // produce the MicroProfileConfigServiceCatalog properties for the service discovery property values if any. - discoveryProperties := generateDiscoveryProperties(a.ctx, a.catalog, props, a.workflow) - if discoveryProperties.Len() > 0 { - props.Merge(discoveryProperties) - } + discoveryProps.Merge(generateDiscoveryProperties(a.ctx, a.catalog, userProps, a.workflow)) } - props = utils.NewApplicationPropertiesBuilder(). - WithInitialProperties(props). + userProps = utils.NewApplicationPropertiesBuilder(). + WithInitialProperties(discoveryProps). WithImmutableProperties(properties.MustLoadString(immutableApplicationProperties)). WithDefaultMutableProperties(a.defaultMutableProperties). Build() - return props.String() + return userProps.String() } // withKogitoServiceUrl adds the property kogitoServiceUrlProperty to the application properties. @@ -135,13 +133,14 @@ func (a *appPropertyHandler) addDefaultMutableProperty(name string, value string } // NewAppPropertyHandler creates a property handler for a given workflow to execute in the provided platform. -// This handler is intended to build the application properties required by the workflow to execute properly, note that -// the produced properties might vary depending on the platfom, for example, if the job service managed by the platform +// This handler is intended to build the managed application properties required by the workflow to execute properly together with +// the user properties defined in the user-managed ConfigMap. +// Note that the produced properties might vary depending on the platfom, for example, if the job service managed by the platform // a particular set of properties will be added, etc. // By default, the following properties are incorporated: // The set of immutable properties provided by the operator. (user can never change) -// The set of defaultMutableProperties that are provided by the operator, and that the user might overwrite if it changes -// the workflow ConfigMap. This set includes for example the required properties to connect with the data index and the +// The set of defaultMutableProperties that are provided by the operator, and that the user cannot overwrite even if it changes +// the user-managed ConfigMap. This set includes for example the required properties to connect with the data index and the // job service when any of these services are managed by the platform. func NewAppPropertyHandler(workflow *operatorapi.SonataFlow, platform *operatorapi.SonataFlowPlatform) (AppPropertyHandler, error) { handler := &appPropertyHandler{ diff --git a/controllers/profiles/common/properties/application_test.go b/controllers/profiles/common/properties/application_test.go index 543247dd7..d0f4d5dff 100644 --- a/controllers/profiles/common/properties/application_test.go +++ b/controllers/profiles/common/properties/application_test.go @@ -124,9 +124,9 @@ func Test_appPropertyHandler_WithUserPropertiesWithNoUserOverrides(t *testing.T) assert.NoError(t, err) generatedProps, propsErr := properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 9, len(generatedProps.Keys())) - assert.Equal(t, "value1", generatedProps.GetString("property1", "")) - assert.Equal(t, "value2", generatedProps.GetString("property2", "")) + assert.Equal(t, 7, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) assert.Equal(t, "http://greeting.default", generatedProps.GetString("kogito.service.url", "")) assert.Equal(t, "8080", generatedProps.GetString("quarkus.http.port", "")) assert.Equal(t, "0.0.0.0", generatedProps.GetString("quarkus.http.host", "")) @@ -157,13 +157,16 @@ func Test_appPropertyHandler_WithUserPropertiesWithServiceDiscovery(t *testing.T Build()) generatedProps.DisableExpansion = true assert.NoError(t, propsErr) - assert.Equal(t, 23, len(generatedProps.Keys())) - assertHasProperty(t, generatedProps, "property1", "value1") - assertHasProperty(t, generatedProps, "property2", "value2") - - assertHasProperty(t, generatedProps, "service1", "${kubernetes:services.v1/namespace1/my-service1}") - assertHasProperty(t, generatedProps, "service2", "${kubernetes:services.v1/my-service2}") - assertHasProperty(t, generatedProps, "service3", "${knative:namespace1/my-kn-service1}") + assert.Equal(t, 21, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) + assertHasProperty(t, generatedProps, "service1", myService1Address) + assertHasProperty(t, generatedProps, "service2", myService2Address) + assertHasProperty(t, generatedProps, "service3", myKnService1Address) + assertHasProperty(t, generatedProps, "service4", myKnService2Address) + assertHasProperty(t, generatedProps, "service5", myKnService3Address) + assertHasProperty(t, generatedProps, "broker1", myKnBroker1Address) + assertHasProperty(t, generatedProps, "broker2", myKnBroker2Address) //org.kie.kogito.addons.discovery.kubernetes\:services.v1\/usecase1ยบ/my-service1 below we use the unescaped vale because the properties.LoadString removes them. assertHasProperty(t, generatedProps, "org.kie.kogito.addons.discovery.kubernetes:services.v1/namespace1/my-service1", myService1Address) @@ -214,12 +217,12 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr := properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 13, len(generatedProps.Keys())) - assert.Equal(t, "value1", generatedProps.GetString("property1", "")) - assert.Equal(t, "value2", generatedProps.GetString("property2", "")) + assert.Equal(t, 11, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) - //kogito.service.url takes the user provided value since it's a default mutable property. - assert.Equal(t, "http://myUrl.override.com", generatedProps.GetString("kogito.service.url", "")) + //kogito.service.url is a default immutable property. + assert.Equal(t, "http://greeting.default", generatedProps.GetString("kogito.service.url", "")) //quarkus.http.port remains with the default value since it's immutable. assert.Equal(t, "8080", generatedProps.GetString("quarkus.http.port", "")) assert.Equal(t, "0.0.0.0", generatedProps.GetString("quarkus.http.host", "")) @@ -239,7 +242,9 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr = properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 18, len(generatedProps.Keys())) + assert.Equal(t, 16, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) assert.Equal(t, "http://"+platform.Name+"-"+constants.DataIndexServiceName+"."+platform.Namespace+"/definitions", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsURL, "")) assert.Equal(t, "true", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsEnabled, "")) assert.Equal(t, "http://"+platform.Name+"-"+constants.DataIndexServiceName+"."+platform.Namespace+"/processes", generatedProps.GetString(constants.KogitoProcessInstancesEventsURL, "")) @@ -259,7 +264,9 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr = properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 14, len(generatedProps.Keys())) + assert.Equal(t, 12, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsURL, "")) assert.Equal(t, "false", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsEnabled, "")) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessInstancesEventsURL, "")) @@ -276,7 +283,9 @@ func Test_appPropertyHandler_WithServicesWithUserOverrides(t *testing.T) { assert.NoError(t, err) generatedProps, propsErr = properties.LoadString(props.WithUserProperties(userProperties).Build()) assert.NoError(t, propsErr) - assert.Equal(t, 13, len(generatedProps.Keys())) + assert.Equal(t, 11, len(generatedProps.Keys())) + assert.NotContains(t, "property1", generatedProps.Keys()) + assert.NotContains(t, "property2", generatedProps.Keys()) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsURL, "")) assert.Equal(t, "false", generatedProps.GetString(constants.KogitoProcessDefinitionsEventsEnabled, "")) assert.Equal(t, "", generatedProps.GetString(constants.KogitoProcessInstancesEventsURL, "")) diff --git a/controllers/profiles/common/properties/discovery.go b/controllers/profiles/common/properties/discovery.go index a7663d419..6d2ac71bb 100644 --- a/controllers/profiles/common/properties/discovery.go +++ b/controllers/profiles/common/properties/discovery.go @@ -99,6 +99,8 @@ func generateDiscoveryProperties(ctx context.Context, catalog discovery.ServiceC mpProperty := generateMicroprofileServiceCatalogProperty(plainUri) klog.V(log.I).Infof("Generating microprofile service catalog property %s=%s.", mpProperty, address) result.MustSet(mpProperty, address) + klog.V(log.I).Infof("Overriding the discoverable value as the managed property %s=%s.", k, address) + result.MustSet(k, address) } } } diff --git a/controllers/profiles/common/properties/discovery_test.go b/controllers/profiles/common/properties/discovery_test.go index 4555c6c38..dd502d184 100644 --- a/controllers/profiles/common/properties/discovery_test.go +++ b/controllers/profiles/common/properties/discovery_test.go @@ -62,7 +62,12 @@ func Test_generateDiscoveryProperties(t *testing.T) { Spec: v1alpha08.SonataFlowSpec{Flow: workflow}, }) - assert.Equal(t, result.Len(), 5) + assert.Equal(t, 8, result.Len()) + assertHasProperty(t, result, "service1", myService1Address) + assertHasProperty(t, result, "service2", myService2Address) + assertHasProperty(t, result, "service3", myService3Address) + assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/namespace1\\/my-service1", myService1Address) + assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/namespace1\\/my-service1", myService1Address) assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/namespace1\\/my-service1", myService1Address) assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/my-service2", myService2Address) assertHasProperty(t, result, "org.kie.kogito.addons.discovery.kubernetes\\:services.v1\\/my-service3?port\\=http-port", myService3Address) diff --git a/controllers/profiles/dev/object_creators_dev.go b/controllers/profiles/dev/object_creators_dev.go index 1cbb42e4c..03b857502 100644 --- a/controllers/profiles/dev/object_creators_dev.go +++ b/controllers/profiles/dev/object_creators_dev.go @@ -118,7 +118,7 @@ func ensureWorkflowDefConfigMapMutator(workflow *operatorapi.SonataFlow) common. } // mountDevConfigMapsMutateVisitor mounts the required configMaps in the Workflow Dev Deployment -func mountDevConfigMapsMutateVisitor(flowDefCM, propsCM *corev1.ConfigMap, workflowResCMs []operatorapi.ConfigMapWorkflowResource) common.MutateVisitor { +func mountDevConfigMapsMutateVisitor(workflow *operatorapi.SonataFlow, flowDefCM, userPropsCM, managedPropsCM *corev1.ConfigMap, workflowResCMs []operatorapi.ConfigMapWorkflowResource) common.MutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { deployment := object.(*appsv1.Deployment) @@ -129,7 +129,8 @@ func mountDevConfigMapsMutateVisitor(flowDefCM, propsCM *corev1.ConfigMap, workf // defaultResourcesVolume holds every ConfigMap mount required on src/main/resources defaultResourcesVolume := corev1.Volume{Name: configMapResourcesVolumeName, VolumeSource: corev1.VolumeSource{Projected: &corev1.ProjectedVolumeSource{}}} - kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, propsCM.Name, corev1.KeyToPath{Key: workflowproj.ApplicationPropertiesFileName, Path: workflowproj.ApplicationPropertiesFileName}) + kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, userPropsCM.Name, corev1.KeyToPath{Key: workflowproj.ApplicationPropertiesFileName, Path: workflowproj.ApplicationPropertiesFileName}) + kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, managedPropsCM.Name, corev1.KeyToPath{Key: workflowproj.GetManagedPropertiesFileName(workflow), Path: workflowproj.GetManagedPropertiesFileName(workflow)}) kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, flowDefCM.Name) // resourceVolumes holds every resource that needs to be mounted on src/main/resources/ diff --git a/controllers/profiles/dev/object_creators_dev_test.go b/controllers/profiles/dev/object_creators_dev_test.go index 37adaf8a4..8209940a2 100644 --- a/controllers/profiles/dev/object_creators_dev_test.go +++ b/controllers/profiles/dev/object_creators_dev_test.go @@ -30,7 +30,6 @@ import ( func Test_ensureWorkflowDevServiceIsExposed(t *testing.T) { workflow := test.GetBaseSonataFlowWithDevProfile(t.Name()) - //On Kubernetes we want the service exposed in Dev with NodePort service, _ := serviceCreator(workflow) service.SetUID("1") diff --git a/controllers/profiles/dev/profile_dev.go b/controllers/profiles/dev/profile_dev.go index dd0ae728d..42b9151ab 100644 --- a/controllers/profiles/dev/profile_dev.go +++ b/controllers/profiles/dev/profile_dev.go @@ -75,21 +75,23 @@ func NewProfileReconciler(client client.Client, cfg *rest.Config, recorder recor func newObjectEnsurers(support *common.StateSupport) *objectEnsurers { return &objectEnsurers{ - deployment: common.NewObjectEnsurer(support.C, deploymentCreator), - service: common.NewObjectEnsurer(support.C, serviceCreator), - network: common.NewNoopObjectEnsurer(), - definitionConfigMap: common.NewObjectEnsurer(support.C, workflowDefConfigMapCreator), - propertiesConfigMap: common.NewObjectEnsurer(support.C, common.WorkflowPropsConfigMapCreator), + deployment: common.NewObjectEnsurer(support.C, deploymentCreator), + service: common.NewObjectEnsurer(support.C, serviceCreator), + network: common.NewNoopObjectEnsurer(), + definitionConfigMap: common.NewObjectEnsurer(support.C, workflowDefConfigMapCreator), + userPropsConfigMap: common.NewObjectEnsurer(support.C, common.UserPropsConfigMapCreator), + managedPropsConfigMap: common.NewObjectEnsurerWithPlatform(support.C, common.ManagedPropsConfigMapCreator), } } func newObjectEnsurersOpenShift(support *common.StateSupport) *objectEnsurers { return &objectEnsurers{ - deployment: common.NewObjectEnsurer(support.C, deploymentCreator), - service: common.NewObjectEnsurer(support.C, serviceCreator), - network: common.NewObjectEnsurer(support.C, common.OpenShiftRouteCreator), - definitionConfigMap: common.NewObjectEnsurer(support.C, workflowDefConfigMapCreator), - propertiesConfigMap: common.NewObjectEnsurer(support.C, common.WorkflowPropsConfigMapCreator), + deployment: common.NewObjectEnsurer(support.C, deploymentCreator), + service: common.NewObjectEnsurer(support.C, serviceCreator), + network: common.NewObjectEnsurer(support.C, common.OpenShiftRouteCreator), + definitionConfigMap: common.NewObjectEnsurer(support.C, workflowDefConfigMapCreator), + userPropsConfigMap: common.NewObjectEnsurer(support.C, common.UserPropsConfigMapCreator), + managedPropsConfigMap: common.NewObjectEnsurerWithPlatform(support.C, common.ManagedPropsConfigMapCreator), } } @@ -106,11 +108,12 @@ func newStatusEnrichersOpenShift(support *common.StateSupport) *statusEnrichers } type objectEnsurers struct { - deployment common.ObjectEnsurer - service common.ObjectEnsurer - network common.ObjectEnsurer - definitionConfigMap common.ObjectEnsurer - propertiesConfigMap common.ObjectEnsurer + deployment common.ObjectEnsurer + service common.ObjectEnsurer + network common.ObjectEnsurer + definitionConfigMap common.ObjectEnsurer + userPropsConfigMap common.ObjectEnsurer + managedPropsConfigMap common.ObjectEnsurerWithPlatform } type statusEnrichers struct { diff --git a/controllers/profiles/dev/profile_dev_test.go b/controllers/profiles/dev/profile_dev_test.go index 5c2591d0b..1c3478ea0 100644 --- a/controllers/profiles/dev/profile_dev_test.go +++ b/controllers/profiles/dev/profile_dev_test.go @@ -145,12 +145,16 @@ func Test_newDevProfile(t *testing.T) { assert.Equal(t, quarkusDevConfigMountPath, deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath) assert.Equal(t, "", deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].SubPath) //https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically - propCM := &corev1.ConfigMap{} - _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowPropertiesConfigMapName(workflow)}, propCM) - assert.NotEmpty(t, propCM.Data[workflowproj.ApplicationPropertiesFileName]) + userPropsCM := &corev1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowUserPropertiesConfigMapName(workflow)}, userPropsCM) + assert.Empty(t, userPropsCM.Data[workflowproj.ApplicationPropertiesFileName]) + + managedPropsCM := &corev1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowManagedPropertiesConfigMapName(workflow)}, managedPropsCM) + assert.NotEmpty(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) assert.Equal(t, quarkusDevConfigMountPath, deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].MountPath) assert.Equal(t, "", deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].SubPath) //https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically - assert.Contains(t, propCM.Data[workflowproj.ApplicationPropertiesFileName], "quarkus.http.port") + assert.Contains(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)], "quarkus.http.port") service := test.MustGetService(t, client, workflow) assert.Equal(t, int32(constants.DefaultHTTPWorkflowPortInt), service.Spec.Ports[0].TargetPort.IntVal) @@ -179,10 +183,14 @@ func Test_newDevProfile(t *testing.T) { err = client.Update(context.TODO(), deployment) assert.NoError(t, err) - propCM = &corev1.ConfigMap{} - _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowPropertiesConfigMapName(workflow)}, propCM) - assert.NotEmpty(t, propCM.Data[workflowproj.ApplicationPropertiesFileName]) - assert.Contains(t, propCM.Data[workflowproj.ApplicationPropertiesFileName], "quarkus.http.port") + userPropsCM = &corev1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowUserPropertiesConfigMapName(workflow)}, userPropsCM) + assert.Empty(t, userPropsCM.Data[workflowproj.ApplicationPropertiesFileName]) + + managedPropsCM = &corev1.ConfigMap{} + _ = client.Get(context.TODO(), types.NamespacedName{Namespace: workflow.Namespace, Name: workflowproj.GetWorkflowManagedPropertiesConfigMapName(workflow)}, managedPropsCM) + assert.NotEmpty(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)]) + assert.Contains(t, managedPropsCM.Data[workflowproj.GetManagedPropertiesFileName(workflow)], "quarkus.http.port") // reconcile workflow.Status.Manager().MarkTrue(api.RunningConditionType) diff --git a/controllers/profiles/dev/states_dev.go b/controllers/profiles/dev/states_dev.go index e965547ef..f98e03eb2 100644 --- a/controllers/profiles/dev/states_dev.go +++ b/controllers/profiles/dev/states_dev.go @@ -74,11 +74,15 @@ func (e *ensureRunningWorkflowState) Do(ctx context.Context, workflow *operatora if err == nil && len(pl.Spec.DevMode.BaseImage) > 0 { devBaseContainerImage = pl.Spec.DevMode.BaseImage } - propsCM, _, err := e.ensurers.propertiesConfigMap.Ensure(ctx, workflow, common.WorkflowPropertiesMutateVisitor(ctx, e.StateSupport.Catalog, workflow, pl)) + userPropsCM, _, err := e.ensurers.userPropsConfigMap.Ensure(ctx, workflow) if err != nil { return ctrl.Result{Requeue: false}, objs, err } - objs = append(objs, propsCM) + managedPropsCM, _, err := e.ensurers.managedPropsConfigMap.Ensure(ctx, workflow, pl, common.ManagedPropertiesMutateVisitor(ctx, e.StateSupport.Catalog, workflow, pl, userPropsCM.(*corev1.ConfigMap))) + if err != nil { + return ctrl.Result{Requeue: false}, objs, err + } + objs = append(objs, managedPropsCM) externalCM, err := workflowdef.FetchExternalResourcesConfigMapsRef(e.C, workflow) if err != nil { @@ -92,7 +96,7 @@ func (e *ensureRunningWorkflowState) Do(ctx context.Context, workflow *operatora deployment, _, err := e.ensurers.deployment.Ensure(ctx, workflow, deploymentMutateVisitor(workflow), common.ImageDeploymentMutateVisitor(workflow, devBaseContainerImage), - mountDevConfigMapsMutateVisitor(flowDefCM.(*corev1.ConfigMap), propsCM.(*corev1.ConfigMap), externalCM)) + mountDevConfigMapsMutateVisitor(workflow, flowDefCM.(*corev1.ConfigMap), userPropsCM.(*corev1.ConfigMap), managedPropsCM.(*corev1.ConfigMap), externalCM)) if err != nil { return ctrl.Result{RequeueAfter: constants.RequeueAfterFailure}, objs, err } diff --git a/controllers/profiles/prod/deployment_handler.go b/controllers/profiles/prod/deployment_handler.go index a5459a873..3b693429d 100644 --- a/controllers/profiles/prod/deployment_handler.go +++ b/controllers/profiles/prod/deployment_handler.go @@ -49,9 +49,16 @@ func (d *deploymentReconciler) reconcile(ctx context.Context, workflow *operator func (d *deploymentReconciler) reconcileWithBuiltImage(ctx context.Context, workflow *operatorapi.SonataFlow, image string) (reconcile.Result, []client.Object, error) { pl, _ := platform.GetActivePlatform(ctx, d.C, workflow.Namespace) - propsCM, _, err := d.ensurers.propertiesConfigMap.Ensure(ctx, workflow, common.WorkflowPropertiesMutateVisitor(ctx, d.StateSupport.Catalog, workflow, pl)) + userPropsCM, _, err := d.ensurers.userPropsConfigMap.Ensure(ctx, workflow) if err != nil { - workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.ExternalResourcesNotFoundReason, "Unable to retrieve the properties config map") + workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.ExternalResourcesNotFoundReason, "Unable to retrieve the user properties config map") + _, err = d.PerformStatusUpdate(ctx, workflow) + return ctrl.Result{}, nil, err + } + managedPropsCM, _, err := d.ensurers.managedPropsConfigMap.Ensure(ctx, workflow, pl, + common.ManagedPropertiesMutateVisitor(ctx, d.StateSupport.Catalog, workflow, pl, userPropsCM.(*v1.ConfigMap))) + if err != nil { + workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.ExternalResourcesNotFoundReason, "Unable to retrieve the managed properties config map") _, err = d.PerformStatusUpdate(ctx, workflow) return ctrl.Result{}, nil, err } @@ -60,7 +67,7 @@ func (d *deploymentReconciler) reconcileWithBuiltImage(ctx context.Context, work d.ensurers.deployment.Ensure( ctx, workflow, - d.getDeploymentMutateVisitors(workflow, image, propsCM.(*v1.ConfigMap))..., + d.getDeploymentMutateVisitors(workflow, image, userPropsCM.(*v1.ConfigMap), managedPropsCM.(*v1.ConfigMap))..., ) if err != nil { workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.DeploymentUnavailableReason, "Unable to perform the deploy due to ", err) @@ -75,7 +82,7 @@ func (d *deploymentReconciler) reconcileWithBuiltImage(ctx context.Context, work return reconcile.Result{}, nil, err } - objs := []client.Object{deployment, service, propsCM} + objs := []client.Object{deployment, service, managedPropsCM} if deploymentOp == controllerutil.OperationResultCreated { workflow.Status.Manager().MarkFalse(api.RunningConditionType, api.WaitingForDeploymentReason, "") @@ -100,17 +107,18 @@ func (d *deploymentReconciler) reconcileWithBuiltImage(ctx context.Context, work func (d *deploymentReconciler) getDeploymentMutateVisitors( workflow *operatorapi.SonataFlow, image string, - configMap *v1.ConfigMap) []common.MutateVisitor { + userPropsCM *v1.ConfigMap, + managedPropsCM *v1.ConfigMap) []common.MutateVisitor { if utils.IsOpenShift() { return []common.MutateVisitor{common.DeploymentMutateVisitor(workflow), - mountProdConfigMapsMutateVisitor(configMap), + mountProdConfigMapsMutateVisitor(workflow, userPropsCM, managedPropsCM), addOpenShiftImageTriggerDeploymentMutateVisitor(workflow, image), common.ImageDeploymentMutateVisitor(workflow, image), - common.RolloutDeploymentIfCMChangedMutateVisitor(configMap), + common.RolloutDeploymentIfCMChangedMutateVisitor(workflow, userPropsCM, managedPropsCM), } } return []common.MutateVisitor{common.DeploymentMutateVisitor(workflow), common.ImageDeploymentMutateVisitor(workflow, image), - mountProdConfigMapsMutateVisitor(configMap), - common.RolloutDeploymentIfCMChangedMutateVisitor(configMap)} + mountProdConfigMapsMutateVisitor(workflow, userPropsCM, managedPropsCM), + common.RolloutDeploymentIfCMChangedMutateVisitor(workflow, userPropsCM, managedPropsCM)} } diff --git a/controllers/profiles/prod/deployment_handler_test.go b/controllers/profiles/prod/deployment_handler_test.go index 5adfae686..a133b9635 100644 --- a/controllers/profiles/prod/deployment_handler_test.go +++ b/controllers/profiles/prod/deployment_handler_test.go @@ -26,6 +26,7 @@ import ( "github.com/stretchr/testify/assert" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) @@ -77,8 +78,12 @@ func Test_CheckDeploymentRolloutAfterCMChange(t *testing.T) { assert.NotEmpty(t, objects) assert.True(t, result.Requeue) + userPropsCM := &corev1.ConfigMap{} + err = client.Get(context.TODO(), types.NamespacedName{Name: workflowproj.GetWorkflowUserPropertiesConfigMapName(workflow), Namespace: t.Name()}, userPropsCM) + assert.NoError(t, err) + // Second reconciliation, we do change the configmap and that must rollout the deployment - var cm *corev1.ConfigMap + var managedPropsCM *corev1.ConfigMap var checksum string for _, o := range objects { if _, ok := o.(*v1.Deployment); ok { @@ -89,16 +94,20 @@ func Test_CheckDeploymentRolloutAfterCMChange(t *testing.T) { assert.NotContains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.RestartedAt) } if _, ok := o.(*corev1.ConfigMap); ok { - cm = o.(*corev1.ConfigMap) - currentProps := cm.Data[workflowproj.ApplicationPropertiesFileName] - props, err := properties.LoadString(currentProps) - assert.Nil(t, err) - props.MustSet("test.property", "test.value") - cm.Data[workflowproj.ApplicationPropertiesFileName] = props.String() + cm := o.(*corev1.ConfigMap) + if cm.Name == workflowproj.GetWorkflowManagedPropertiesConfigMapName(workflow) { + managedPropsCM = cm + } } } - assert.NotNil(t, cm) - utilruntime.Must(client.Update(context.TODO(), cm)) + assert.NotNil(t, managedPropsCM) + + currentProps := userPropsCM.Data[workflowproj.ApplicationPropertiesFileName] + props, err := properties.LoadString(currentProps) + assert.Nil(t, err) + props.MustSet("test.property", "test.value") + userPropsCM.Data[workflowproj.ApplicationPropertiesFileName] = props.String() + utilruntime.Must(client.Update(context.TODO(), userPropsCM)) result, objects, err = handler.reconcile(context.TODO(), workflow) assert.NoError(t, err) assert.NotEmpty(t, objects) @@ -131,9 +140,13 @@ func Test_CheckDeploymentUnchangedAfterCMChangeOtherKeys(t *testing.T) { assert.NotEmpty(t, objects) assert.True(t, result.Requeue) + userPropsCM := &corev1.ConfigMap{} + err = client.Get(context.TODO(), types.NamespacedName{Name: workflowproj.GetWorkflowUserPropertiesConfigMapName(workflow), Namespace: t.Name()}, userPropsCM) + assert.NoError(t, err) + // Second reconciliation, we do change the configmap and that must not rollout the deployment // because we're not updating the application.properties key - var cm *corev1.ConfigMap + var managedPropsCM *corev1.ConfigMap var checksum string for _, o := range objects { if _, ok := o.(*v1.Deployment); ok { @@ -144,12 +157,16 @@ func Test_CheckDeploymentUnchangedAfterCMChangeOtherKeys(t *testing.T) { assert.NotContains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.RestartedAt) } if _, ok := o.(*corev1.ConfigMap); ok { - cm = o.(*corev1.ConfigMap) - cm.Data["other.key"] = "useless.key = value" + cm := o.(*corev1.ConfigMap) + if cm.Name == workflowproj.GetWorkflowManagedPropertiesConfigMapName(workflow) { + managedPropsCM = cm + } } } - assert.NotNil(t, cm) - utilruntime.Must(client.Update(context.TODO(), cm)) + assert.NotNil(t, managedPropsCM) + + userPropsCM.Data["other.key"] = "useless.key = value" + utilruntime.Must(client.Update(context.TODO(), userPropsCM)) result, objects, err = handler.reconcile(context.TODO(), workflow) assert.NoError(t, err) assert.NotEmpty(t, objects) @@ -157,13 +174,10 @@ func Test_CheckDeploymentUnchangedAfterCMChangeOtherKeys(t *testing.T) { for _, o := range objects { if _, ok := o.(*v1.Deployment); ok { deployment := o.(*v1.Deployment) - // Commented while waiting for SRVLOGIC-195 to be addressed - // assert.NotContains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.RestartedAt) - assert.Contains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.Checksum) + assert.NotContains(t, deployment.Spec.Template.ObjectMeta.Annotations, metadata.RestartedAt) newChecksum := deployment.Spec.Template.ObjectMeta.Annotations[metadata.Checksum] assert.NotEmpty(t, newChecksum) - // Change to asssert.Equal when SRVLOGIC-195 is addressed - assert.NotEqual(t, newChecksum, checksum) + assert.Equal(t, newChecksum, checksum) break } } diff --git a/controllers/profiles/prod/object_creators_prod.go b/controllers/profiles/prod/object_creators_prod.go index 34d1447ec..02328e9b4 100644 --- a/controllers/profiles/prod/object_creators_prod.go +++ b/controllers/profiles/prod/object_creators_prod.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles/common" "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles/common/constants" @@ -64,7 +65,7 @@ func addOpenShiftImageTriggerDeploymentMutateVisitor(workflow *v1alpha08.SonataF } // mountDevConfigMapsMutateVisitor mounts the required configMaps in the Workflow Dev Deployment -func mountProdConfigMapsMutateVisitor(propsCM *v1.ConfigMap) common.MutateVisitor { +func mountProdConfigMapsMutateVisitor(workflow *operatorapi.SonataFlow, userPropsCM *v1.ConfigMap, managedPropsCM *v1.ConfigMap) common.MutateVisitor { return func(object client.Object) controllerutil.MutateFn { return func() error { deployment := object.(*appsv1.Deployment) @@ -77,12 +78,14 @@ func mountProdConfigMapsMutateVisitor(propsCM *v1.ConfigMap) common.MutateVisito deployment.Spec.Template.Spec.Containers[idx].VolumeMounts = make([]v1.VolumeMount, 0, 1) } - kubeutil.AddOrReplaceVolume(&deployment.Spec.Template.Spec, - kubeutil.VolumeConfigMap(constants.ConfigMapWorkflowPropsVolumeName, propsCM.Name, v1.KeyToPath{Key: workflowproj.ApplicationPropertiesFileName, Path: workflowproj.ApplicationPropertiesFileName})) + defaultResourcesVolume := v1.Volume{Name: constants.ConfigMapWorkflowPropsVolumeName, VolumeSource: v1.VolumeSource{Projected: &v1.ProjectedVolumeSource{}}} + kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, userPropsCM.Name, v1.KeyToPath{Key: workflowproj.ApplicationPropertiesFileName, Path: workflowproj.ApplicationPropertiesFileName}) + kubeutil.VolumeProjectionAddConfigMap(defaultResourcesVolume.Projected, managedPropsCM.Name, v1.KeyToPath{Key: workflowproj.GetManagedPropertiesFileName(workflow), Path: workflowproj.GetManagedPropertiesFileName(workflow)}) + kubeutil.AddOrReplaceVolume(&deployment.Spec.Template.Spec, defaultResourcesVolume) kubeutil.AddOrReplaceVolumeMount(idx, &deployment.Spec.Template.Spec, kubeutil.VolumeMount(constants.ConfigMapWorkflowPropsVolumeName, true, quarkusProdConfigMountPath)) - kubeutil.AnnotateDeploymentConfigChecksum(deployment, propsCM) + kubeutil.AnnotateDeploymentConfigChecksum(workflow, deployment, userPropsCM, managedPropsCM) return nil } } diff --git a/controllers/profiles/prod/profile_prod.go b/controllers/profiles/prod/profile_prod.go index 062359ee1..f5046d05f 100644 --- a/controllers/profiles/prod/profile_prod.go +++ b/controllers/profiles/prod/profile_prod.go @@ -52,16 +52,18 @@ const ( // ReconciliationState that needs access to it must include this struct as an attribute and initialize it in the profile builder. // Use newObjectEnsurers to facilitate building this struct type objectEnsurers struct { - deployment common.ObjectEnsurer - service common.ObjectEnsurer - propertiesConfigMap common.ObjectEnsurer + deployment common.ObjectEnsurer + service common.ObjectEnsurer + userPropsConfigMap common.ObjectEnsurer + managedPropsConfigMap common.ObjectEnsurerWithPlatform } func newObjectEnsurers(support *common.StateSupport) *objectEnsurers { return &objectEnsurers{ - deployment: common.NewObjectEnsurer(support.C, common.DeploymentCreator), - service: common.NewObjectEnsurer(support.C, common.ServiceCreator), - propertiesConfigMap: common.NewObjectEnsurer(support.C, common.WorkflowPropsConfigMapCreator), + deployment: common.NewObjectEnsurer(support.C, common.DeploymentCreator), + service: common.NewObjectEnsurer(support.C, common.ServiceCreator), + userPropsConfigMap: common.NewObjectEnsurer(support.C, common.UserPropsConfigMapCreator), + managedPropsConfigMap: common.NewObjectEnsurerWithPlatform(support.C, common.ManagedPropsConfigMapCreator), } } diff --git a/utils/kubernetes/deployment.go b/utils/kubernetes/deployment.go index ab8dd9409..676c49924 100644 --- a/utils/kubernetes/deployment.go +++ b/utils/kubernetes/deployment.go @@ -27,6 +27,7 @@ import ( "time" "github.com/apache/incubator-kie-kogito-serverless-operator/api/metadata" + operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" "github.com/apache/incubator-kie-kogito-serverless-operator/log" "github.com/apache/incubator-kie-kogito-serverless-operator/workflowproj" appsv1 "k8s.io/api/apps/v1" @@ -112,7 +113,7 @@ func MarkDeploymentToRollout(deployment *appsv1.Deployment) error { // AnnotateDeploymentConfigChecksum adds the checksum/config annotation to the template annotations of the Deployment to set the current configuration. // If the checksum has changed from the previous value, the restartedAt annotation is also added and a new rollout is started. // Code adapted from here: https://github.com/kubernetes/kubectl/blob/release-1.26/pkg/polymorphichelpers/objectrestarter.go#L44 -func AnnotateDeploymentConfigChecksum(deployment *appsv1.Deployment, cm *v1.ConfigMap) error { +func AnnotateDeploymentConfigChecksum(workflow *operatorapi.SonataFlow, deployment *appsv1.Deployment, userPropsCM *v1.ConfigMap, managedPropsCM *v1.ConfigMap) error { if deployment.Spec.Paused { return errors.New("can't restart paused deployment (run rollout resume first)") } @@ -124,7 +125,7 @@ func AnnotateDeploymentConfigChecksum(deployment *appsv1.Deployment, cm *v1.Conf if !ok { currentChecksum = "" } - newChecksum, err := configMapChecksum(cm) + newChecksum, err := calculateHash(userPropsCM, managedPropsCM, workflow) if err != nil { return err } @@ -141,14 +142,19 @@ func AnnotateDeploymentConfigChecksum(deployment *appsv1.Deployment, cm *v1.Conf return nil } -func configMapChecksum(cm *v1.ConfigMap) (string, error) { - props, hasKey := cm.Data[workflowproj.ApplicationPropertiesFileName] +func dataFromCM(cm *v1.ConfigMap, key string) string { + data, hasKey := cm.Data[key] if !hasKey { - props = "" + return "" } + return data +} +func calculateHash(userPropsCM, managedPropsCM *v1.ConfigMap, workflow *operatorapi.SonataFlow) (string, error) { + aggregatedProps := fmt.Sprintf("%s,%s", dataFromCM(userPropsCM, workflowproj.ApplicationPropertiesFileName), + dataFromCM(managedPropsCM, workflowproj.GetManagedPropertiesFileName(workflow))) hash := sha256.New() - _, err := hash.Write([]byte(props)) + _, err := hash.Write([]byte(aggregatedProps)) if err != nil { return "", err } diff --git a/workflowproj/operator.go b/workflowproj/operator.go index 7821c36ae..33b4ca672 100644 --- a/workflowproj/operator.go +++ b/workflowproj/operator.go @@ -20,18 +20,22 @@ package workflowproj import ( + "fmt" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "github.com/apache/incubator-kie-kogito-serverless-operator/api/metadata" operatorapi "github.com/apache/incubator-kie-kogito-serverless-operator/api/v1alpha08" + "github.com/apache/incubator-kie-kogito-serverless-operator/controllers/profiles" ) const ( - workflowConfigMapNameSuffix = "-props" - // ApplicationPropertiesFileName is the default application properties file name - ApplicationPropertiesFileName = "application.properties" + workflowUserConfigMapNameSuffix = "-props" + // ApplicationPropertiesFileName is the default application properties file name holding user properties + ApplicationPropertiesFileName = "application.properties" + workflowManagedConfigMapNameSuffix = "-managed-props" // LabelApp key to use among object selectors, "app" is used among k8s applications to group objects in some UI consoles LabelApp = "app" // LabelService key to use among object selectors @@ -60,9 +64,23 @@ func SetTypeToObject(obj runtime.Object, s *runtime.Scheme) error { return nil } -// GetWorkflowPropertiesConfigMapName gets the default ConfigMap name that holds the application property for the given workflow -func GetWorkflowPropertiesConfigMapName(workflow *operatorapi.SonataFlow) string { - return workflow.Name + workflowConfigMapNameSuffix +// GetWorkflowUserPropertiesConfigMapName gets the default ConfigMap name that holds the user application property for the given workflow +func GetWorkflowUserPropertiesConfigMapName(workflow *operatorapi.SonataFlow) string { + return workflow.Name + workflowUserConfigMapNameSuffix +} + +// GetWorkflowManagedPropertiesConfigMapName gets the default ConfigMap name that holds the managed application property for the given workflow +func GetWorkflowManagedPropertiesConfigMapName(workflow *operatorapi.SonataFlow) string { + return workflow.Name + workflowManagedConfigMapNameSuffix +} + +// GetWorkflowManagedPropertiesConfigMapName gets the default ConfigMap name that holds the managed application property for the given workflow +func GetManagedPropertiesFileName(workflow *operatorapi.SonataFlow) string { + profile := metadata.ProdProfile + if profiles.IsDevProfile(workflow) { + profile = metadata.DevProfile + } + return fmt.Sprintf("application-%s.properties", profile) } // SetDefaultLabels adds the default workflow application labels to the given object. @@ -80,15 +98,27 @@ func GetDefaultLabels(workflow *operatorapi.SonataFlow) map[string]string { } } -// CreateNewAppPropsConfigMap creates a new ConfigMap object to hold the workflow application properties. -func CreateNewAppPropsConfigMap(workflow *operatorapi.SonataFlow, properties string) *corev1.ConfigMap { +// CreateNewUserPropsConfigMap creates a new empty ConfigMap object to hold the user application properties of the workflow. +func CreateNewUserPropsConfigMap(workflow *operatorapi.SonataFlow) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetWorkflowUserPropertiesConfigMapName(workflow), + Namespace: workflow.Namespace, + Labels: GetDefaultLabels(workflow), + }, + Data: map[string]string{ApplicationPropertiesFileName: ""}, + } +} + +// CreateNewManagedPropsConfigMap creates a new ConfigMap object to hold the managed application properties of the workflos. +func CreateNewManagedPropsConfigMap(workflow *operatorapi.SonataFlow, properties string) *corev1.ConfigMap { return &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: GetWorkflowPropertiesConfigMapName(workflow), + Name: GetWorkflowManagedPropertiesConfigMapName(workflow), Namespace: workflow.Namespace, Labels: GetDefaultLabels(workflow), }, - Data: map[string]string{ApplicationPropertiesFileName: properties}, + Data: map[string]string{GetManagedPropertiesFileName(workflow): properties}, } } diff --git a/workflowproj/workflowproj.go b/workflowproj/workflowproj.go index 3b46f8c95..afabd7e5c 100644 --- a/workflowproj/workflowproj.go +++ b/workflowproj/workflowproj.go @@ -247,7 +247,8 @@ func (w *workflowProjectHandler) parseRawAppProperties() error { if err != nil { return err } - w.project.Properties = CreateNewAppPropsConfigMap(w.project.Workflow, string(appPropsContent)) + w.project.Properties = CreateNewUserPropsConfigMap(w.project.Workflow) + w.project.Properties.Data[ApplicationPropertiesFileName] = string(appPropsContent) if err = SetTypeToObject(w.project.Properties, w.scheme); err != nil { return err } diff --git a/workflowproj/workflowproj_test.go b/workflowproj/workflowproj_test.go index 0bc9679cf..c1207527f 100644 --- a/workflowproj/workflowproj_test.go +++ b/workflowproj/workflowproj_test.go @@ -61,6 +61,7 @@ func Test_Handler_WorkflowMinimalAndProps(t *testing.T) { assert.NotNil(t, proj.Properties) assert.Equal(t, "minimal", proj.Workflow.Name) assert.Equal(t, "minimal-props", proj.Properties.Name) + assert.NotEmpty(t, proj.Properties.Data["application.properties"]) assert.Equal(t, string(metadata.ProdProfile), proj.Workflow.Annotations[metadata.Profile]) assert.NotEmpty(t, proj.Properties.Data) }