diff --git a/api/v1beta1/moduletemplate_conversion.go b/api/v1beta1/moduletemplate_conversion.go index bb77956d1d..a6850addb9 100644 --- a/api/v1beta1/moduletemplate_conversion.go +++ b/api/v1beta1/moduletemplate_conversion.go @@ -11,6 +11,7 @@ func (src *ModuleTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Channel = src.Spec.Channel dst.Spec.Data = src.Spec.Data dst.Spec.Descriptor = src.Spec.Descriptor + dst.Spec.CustomStateCheck = (*v1beta2.CustomStateCheck)(src.Spec.CustomStateCheck) return nil } @@ -21,6 +22,7 @@ func (dst *ModuleTemplate) ConvertFrom(srcRaw conversion.Hub) error { dst.Spec.Channel = src.Spec.Channel dst.Spec.Data = src.Spec.Data dst.Spec.Descriptor = src.Spec.Descriptor + dst.Spec.CustomStateCheck = (*CustomStateCheck)(src.Spec.CustomStateCheck) dst.Spec.Target = TargetRemote return nil } diff --git a/api/v1beta2/kyma_types.go b/api/v1beta2/kyma_types.go index d4b76db09e..b4dfc29d30 100644 --- a/api/v1beta2/kyma_types.go +++ b/api/v1beta2/kyma_types.go @@ -179,6 +179,9 @@ type ModuleStatus struct { // Resource contains information about the created module CR. Resource *TrackingObject `json:"resource,omitempty"` + + // CustomStateCheck for advanced Module State determination + CustomStateCheck *CustomStateCheck `json:"customStateCheck,omitempty"` } // TrackingObject contains metav1.TypeMeta and PartialMeta to allow a generation based object tracking. diff --git a/api/v1beta2/moduletemplate_types.go b/api/v1beta2/moduletemplate_types.go index fc72d6bddb..82d99020f5 100644 --- a/api/v1beta2/moduletemplate_types.go +++ b/api/v1beta2/moduletemplate_types.go @@ -99,6 +99,17 @@ type ModuleTemplateSpec struct { // //+kubebuilder:pruning:PreserveUnknownFields Descriptor runtime.RawExtension `json:"descriptor"` + + // CustomStateCheck for advanced Module State determination + CustomStateCheck *CustomStateCheck `json:"customStateCheck,omitempty"` +} + +type CustomStateCheck struct { + // JsonPath specifies the JSON path to the state variable in the Module CR + JsonPath string `json:"jsonPath"` + + // Value is the value at the JsonPath for which the Module CR state is set to "Ready" in Kyma CR + Value string `json:"value"` } func (spec *ModuleTemplateSpec) GetDescriptor(opts ...compdesc.DecodeOption) (*Descriptor, error) { diff --git a/config/crd/bases/operator.kyma-project.io_kymas.yaml b/config/crd/bases/operator.kyma-project.io_kymas.yaml index 262728c3cd..d54d875f05 100644 --- a/config/crd/bases/operator.kyma-project.io_kymas.yaml +++ b/config/crd/bases/operator.kyma-project.io_kymas.yaml @@ -236,6 +236,21 @@ spec: lookup to be necessary that maybe picks a different ModuleTemplate, which is why we need to reconcile. type: string + customStateCheck: + description: CustomStateCheck for advanced Module State determination + properties: + jsonPath: + description: JsonPath specifies the JSON path to the state + variable in the Module CR + type: string + value: + description: Value is the value at the JsonPath for which + the Module CR state is set to "Ready" in Kyma CR + type: string + required: + - jsonPath + - value + type: object fqdn: description: FQDN is the fully qualified domain name of the module. In the ModuleTemplate it is located in .spec.descriptor.component.name @@ -612,6 +627,21 @@ spec: lookup to be necessary that maybe picks a different ModuleTemplate, which is why we need to reconcile. type: string + customStateCheck: + description: CustomStateCheck for advanced Module State determination + properties: + jsonPath: + description: JsonPath specifies the JSON path to the state + variable in the Module CR + type: string + value: + description: Value is the value at the JsonPath for which + the Module CR state is set to "Ready" in Kyma CR + type: string + required: + - jsonPath + - value + type: object fqdn: description: FQDN is the fully qualified domain name of the module. In the ModuleTemplate it is located in .spec.descriptor.component.name diff --git a/config/crd/bases/operator.kyma-project.io_moduletemplates.yaml b/config/crd/bases/operator.kyma-project.io_moduletemplates.yaml index 803b3bc6ab..793ec7c56a 100644 --- a/config/crd/bases/operator.kyma-project.io_moduletemplates.yaml +++ b/config/crd/bases/operator.kyma-project.io_moduletemplates.yaml @@ -49,6 +49,21 @@ spec: minLength: 3 pattern: ^[a-z]+$ type: string + customStateCheck: + description: CustomStateCheck for advanced Module State determination + properties: + jsonPath: + description: JsonPath specifies the JSON path to the state variable + in the Module CR + type: string + value: + description: Value is the value at the JsonPath for which the + Module CR state is set to "Ready" in Kyma CR + type: string + required: + - jsonPath + - value + type: object data: description: Data is the default set of attributes that are used to generate the Module. It contains a default set of values for a given @@ -124,6 +139,21 @@ spec: minLength: 3 pattern: ^[a-z]+$ type: string + customStateCheck: + description: CustomStateCheck for advanced Module State determination + properties: + jsonPath: + description: JsonPath specifies the JSON path to the state variable + in the Module CR + type: string + value: + description: Value is the value at the JsonPath for which the + Module CR state is set to "Ready" in Kyma CR + type: string + required: + - jsonPath + - value + type: object data: description: Data is the default set of attributes that are used to generate the Module. It contains a default set of values for a given diff --git a/controllers/moduletemplate_test.go b/controllers/moduletemplate_test.go index e00fc5b4b6..6ddf38fbdc 100644 --- a/controllers/moduletemplate_test.go +++ b/controllers/moduletemplate_test.go @@ -140,7 +140,7 @@ var _ = Describe("Custom State Check can be used", Ordered, func() { It("Should create manifest", func() { Eventually(ManifestExists, Timeout, Interval). - WithArguments(kyma, module). + WithArguments(ctx, kyma, module, controlPlaneClient). Should(Succeed()) }) diff --git a/go.mod b/go.mod index 8cc00f88ae..a7634995a1 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/open-component-model/ocm v0.2.0 github.com/prometheus/client_golang v1.14.0 github.com/stretchr/testify v1.8.2 + github.com/tidwall/gjson v1.14.4 github.com/xeipuuv/gojsonschema v1.2.0 go.uber.org/zap v1.24.0 golang.org/x/sync v0.1.0 @@ -178,6 +179,8 @@ require ( github.com/spf13/cobra v1.6.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/theupdateframework/notary v0.7.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/tonglil/buflogr v1.0.1 // indirect github.com/ulikunitz/xz v0.5.10 // indirect github.com/vbatts/tar-split v0.11.2 // indirect diff --git a/go.sum b/go.sum index 862b3fa100..c01ef17858 100644 --- a/go.sum +++ b/go.sum @@ -1241,6 +1241,12 @@ github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tonglil/buflogr v1.0.1 h1:WXFZLKxLfqcVSmckwiMCF8jJwjIgmStJmg63YKRF1p0= diff --git a/pkg/module/sync/runner_impl.go b/pkg/module/sync/runner_impl.go index 300ebcbc87..d85fe69585 100644 --- a/pkg/module/sync/runner_impl.go +++ b/pkg/module/sync/runner_impl.go @@ -2,12 +2,15 @@ package sync import ( "context" + "encoding/json" "errors" "fmt" + "strings" "time" "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/pkg/channel" + "github.com/tidwall/gjson" apiErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -161,23 +164,28 @@ func generateModuleStatus(module *common.Module, existStatus *v1beta2.ModuleStat return *newModuleStatus } if module.Template.Err != nil { - return v1beta2.ModuleStatus{ + status := v1beta2.ModuleStatus{ Name: module.ModuleName, Channel: module.Template.DesiredChannel, FQDN: module.FQDN, State: v1beta2.StateError, Message: module.Template.Err.Error(), } + if module.Template.ModuleTemplate != nil { + status.CustomStateCheck = module.Template.Spec.CustomStateCheck + } + return status } manifestObject, ok := module.Object.(*v1beta2.Manifest) if !ok { // TODO: impossible case, remove casting check after module use typed Manifest instead of client.Object return v1beta2.ModuleStatus{ - Name: module.ModuleName, - Channel: module.Template.DesiredChannel, - FQDN: module.FQDN, - State: v1beta2.StateError, - Message: ErrManifestConversion.Error(), + Name: module.ModuleName, + Channel: module.Template.DesiredChannel, + FQDN: module.FQDN, + State: v1beta2.StateError, + Message: ErrManifestConversion.Error(), + CustomStateCheck: module.Template.Spec.CustomStateCheck, } } manifestAPIVersion, manifestKind := manifestObject.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind() @@ -195,7 +203,7 @@ func generateModuleStatus(module *common.Module, existStatus *v1beta2.ModuleStat return v1beta2.ModuleStatus{ Name: module.ModuleName, FQDN: module.FQDN, - State: v1beta2.State(manifestObject.Status.State), + State: stateFromManifest(module.Object, module.Template.Spec.CustomStateCheck), Channel: module.Template.Spec.Channel, Version: module.Version, Manifest: &v1beta2.TrackingObject{ @@ -206,19 +214,42 @@ func generateModuleStatus(module *common.Module, existStatus *v1beta2.ModuleStat PartialMeta: v1beta2.PartialMetaFromObject(module.Template), TypeMeta: metav1.TypeMeta{Kind: templateKind, APIVersion: templateAPIVersion}, }, - Resource: moduleResource, + Resource: moduleResource, + CustomStateCheck: module.Template.Spec.CustomStateCheck, + } +} + +func stateFromManifest(obj client.Object, customStateCheck *v1beta2.CustomStateCheck) v1beta2.State { + if customStateCheck == nil { + switch manifest := obj.(type) { + case *v1beta2.Manifest: + return v1beta2.State(manifest.Status.State) + case *unstructured.Unstructured: + state, _, _ := unstructured.NestedString(manifest.Object, "status", "state") + return v1beta2.State(state) + default: + return "" + } + } else { + return processCustomStateCheck(obj, customStateCheck) } } -func stateFromManifest(obj client.Object) v1beta2.State { - switch manifest := obj.(type) { - case *v1beta2.Manifest: - return v1beta2.State(manifest.Status.State) - case *unstructured.Unstructured: - state, _, _ := unstructured.NestedString(manifest.Object, "status", "state") - return v1beta2.State(state) - default: - return "" +func processCustomStateCheck(obj client.Object, customStateCheck *v1beta2.CustomStateCheck) v1beta2.State { + marshalledObj, err := json.Marshal(obj) + if err != nil { + return v1beta2.StateError + } + + customStateCheck.JsonPath, _ = strings.CutPrefix(customStateCheck.JsonPath, ".") + result := gjson.Get(string(marshalledObj), customStateCheck.JsonPath) + + if valueFromManifest, ok := result.Value().(string); ok && customStateCheck.Value == valueFromManifest { + return v1beta2.StateReady + } else if !result.Exists() { + return v1beta2.StateError + } else { + return v1beta2.StateProcessing } } @@ -243,7 +274,7 @@ func DeleteNoLongerExistingModuleStatus( if apiErrors.IsNotFound(err) { delete(moduleStatusMap, moduleStatus.Name) } else { - moduleStatus.State = stateFromManifest(module) + moduleStatus.State = stateFromManifest(module, moduleStatus.CustomStateCheck) } } kyma.Status.Modules = convertToNewModuleStatus(moduleStatusMap)