diff --git a/apis/apps/v1/componentversion_types.go b/apis/apps/v1/componentversion_types.go index f9641def044..0fa780c7e01 100644 --- a/apis/apps/v1/componentversion_types.go +++ b/apis/apps/v1/componentversion_types.go @@ -141,12 +141,15 @@ type ComponentVersionRelease struct { // +kubebuilder:validation:MaxLength=32 ServiceVersion string `json:"serviceVersion"` - // Images define the new images for different containers within the release. + // Images define the new images for containers, actions or external applications within the release. + // + // If an image is specified for a lifecycle action, the key should be the field name (case-insensitive) of + // the action in the LifecycleActions struct. // // +kubebuilder:validation:Required // +kubebuilder:validation:MinProperties=1 // +kubebuilder:validation:MaxProperties=128 - // +kubebuilder:validation:XValidation:rule="self.all(key, size(key) <= 32)",message="Container name may not exceed maximum length of 32 characters" + // +kubebuilder:validation:XValidation:rule="self.all(key, size(key) <= 32)",message="Container, action or external application name may not exceed maximum length of 32 characters" // +kubebuilder:validation:XValidation:rule="self.all(key, size(self[key]) <= 256)",message="Image name may not exceed maximum length of 256 characters" Images map[string]string `json:"images"` } diff --git a/config/crd/bases/apps.kubeblocks.io_componentversions.yaml b/config/crd/bases/apps.kubeblocks.io_componentversions.yaml index 9e224ef5808..97e945aa7f4 100644 --- a/config/crd/bases/apps.kubeblocks.io_componentversions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_componentversions.yaml @@ -109,14 +109,18 @@ spec: images: additionalProperties: type: string - description: Images define the new images for different containers - within the release. + description: |- + Images define the new images for containers, actions or external applications within the release. + + + If an image is specified for a lifecycle action, the key should be the field name (case-insensitive) of + the action in the LifecycleActions struct. maxProperties: 128 minProperties: 1 type: object x-kubernetes-validations: - - message: Container name may not exceed maximum length of 32 - characters + - message: Container, action or external application name may + not exceed maximum length of 32 characters rule: self.all(key, size(key) <= 32) - message: Image name may not exceed maximum length of 256 characters rule: self.all(key, size(self[key]) <= 256) diff --git a/controllers/apps/componentversion_controller.go b/controllers/apps/componentversion_controller.go index 3cad2300486..3d8f3b1aa48 100644 --- a/controllers/apps/componentversion_controller.go +++ b/controllers/apps/componentversion_controller.go @@ -323,24 +323,12 @@ func (r *ComponentVersionReconciler) validateServiceVersion(release appsv1.Compo } func (r *ComponentVersionReconciler) validateImages(release appsv1.ComponentVersionRelease, cmpds map[string]*appsv1.ComponentDefinition) error { - validateContainer := func(cmpd appsv1.ComponentDefinition, name string) error { - cmp := func(c corev1.Container) bool { - return c.Name == name - } - if slices.IndexFunc(cmpd.Spec.Runtime.InitContainers, cmp) != -1 { - return nil - } - if slices.IndexFunc(cmpd.Spec.Runtime.Containers, cmp) != -1 { - return nil - } - return fmt.Errorf("container %s is not found in ComponentDefinition %s", name, cmpd.Name) - } for name := range release.Images { for _, cmpd := range cmpds { if cmpd == nil { continue } - if err := validateContainer(*cmpd, name); err != nil { + if err := r.validateImageContainer(*cmpd, name); err != nil { return err } } @@ -348,6 +336,45 @@ func (r *ComponentVersionReconciler) validateImages(release appsv1.ComponentVers return nil } +func (r *ComponentVersionReconciler) validateImageContainer(cmpd appsv1.ComponentDefinition, name string) error { + if r.imageDefinedInContainers(cmpd, name) { + return nil + } + if r.imageDefinedInActions(cmpd, name) { + return nil + } + // user-managed images, leave it to the user to handle + return nil +} + +func (r *ComponentVersionReconciler) imageDefinedInContainers(cmpd appsv1.ComponentDefinition, name string) bool { + cmp := func(c corev1.Container) bool { + return c.Name == name + } + if slices.IndexFunc(cmpd.Spec.Runtime.InitContainers, cmp) != -1 { + return true + } + if slices.IndexFunc(cmpd.Spec.Runtime.Containers, cmp) != -1 { + return true + } + return false +} + +func (r *ComponentVersionReconciler) imageDefinedInActions(_ appsv1.ComponentDefinition, name string) bool { + match := func(action string) bool { + // case insensitive + return strings.EqualFold(action, name) + } + + tp := reflect.TypeOf(appsv1.ComponentLifecycleActions{}) + for i := 0; i < tp.NumField(); i++ { + if match(tp.Field(i).Name) { + return true + } + } + return false +} + // validateCompatibilityRulesCompDef validates the reference component definition name pattern defined in compatibility rules. func validateCompatibilityRulesCompDef(compVersion *appsv1.ComponentVersion) error { for _, rule := range compVersion.Spec.CompatibilityRules { diff --git a/controllers/apps/componentversion_controller_test.go b/controllers/apps/componentversion_controller_test.go index d1cc7163484..6cf901680f8 100644 --- a/controllers/apps/componentversion_controller_test.go +++ b/controllers/apps/componentversion_controller_test.go @@ -81,6 +81,8 @@ var _ = Describe("ComponentVersion Controller", func() { // use empty revision as init image tag f = f.SetRuntime(&corev1.Container{Name: app, Image: testapps.AppImage(app, testapps.ReleaseID(""))}) } + f.SetLifecycleAction(testapps.DefaultActionName, + &appsv1.Action{Exec: &appsv1.ExecAction{Image: testapps.AppImage(testapps.DefaultActionName, testapps.ReleaseID(""))}}) objs = append(objs, f.Create(&testCtx).GetObject()) } for _, obj := range objs { @@ -116,6 +118,7 @@ var _ = Describe("ComponentVersion Controller", func() { Images: map[string]string{ testapps.AppName: testapps.AppImage(testapps.AppName, testapps.ReleaseID("r0")), testapps.AppNameSamePrefix: testapps.AppImage(testapps.AppNameSamePrefix, testapps.ReleaseID("r0")), + testapps.DefaultActionName: testapps.AppImage(testapps.DefaultActionName, testapps.ReleaseID("r0")), }, }, { @@ -133,6 +136,7 @@ var _ = Describe("ComponentVersion Controller", func() { Images: map[string]string{ testapps.AppName: testapps.AppImage(testapps.AppName, testapps.ReleaseID("r2")), testapps.AppNameSamePrefix: testapps.AppImage(testapps.AppNameSamePrefix, testapps.ReleaseID("r2")), + testapps.DefaultActionName: testapps.AppImage(testapps.DefaultActionName, testapps.ReleaseID("r2")), }, }, { @@ -150,6 +154,7 @@ var _ = Describe("ComponentVersion Controller", func() { Images: map[string]string{ testapps.AppName: testapps.AppImage(testapps.AppName, testapps.ReleaseID("r4")), testapps.AppNameSamePrefix: testapps.AppImage(testapps.AppNameSamePrefix, testapps.ReleaseID("r4")), + testapps.DefaultActionName: testapps.AppImage(testapps.DefaultActionName, testapps.ReleaseID("r4")), }, }, { @@ -159,6 +164,7 @@ var _ = Describe("ComponentVersion Controller", func() { Images: map[string]string{ testapps.AppName: testapps.AppImage(testapps.AppName, testapps.ReleaseID("r5")), testapps.AppNameSamePrefix: testapps.AppImage(testapps.AppNameSamePrefix, testapps.ReleaseID("r5")), + testapps.DefaultActionName: testapps.AppImage(testapps.DefaultActionName, testapps.ReleaseID("r5")), }, }, }, @@ -180,6 +186,11 @@ var _ = Describe("ComponentVersion Controller", func() { Expect(compDef.Spec.Runtime.Containers).Should(HaveLen(2)) Expect(compDef.Spec.Runtime.Containers[0].Image).Should(Equal(testapps.AppImage(compDef.Spec.Runtime.Containers[0].Name, testapps.ReleaseID(r0)))) Expect(compDef.Spec.Runtime.Containers[1].Image).Should(Equal(testapps.AppImage(compDef.Spec.Runtime.Containers[1].Name, testapps.ReleaseID(r1)))) + + Expect(compDef.Spec.LifecycleActions).ShouldNot(BeNil()) + Expect(compDef.Spec.LifecycleActions.PreTerminate).ShouldNot(BeNil()) + Expect(compDef.Spec.LifecycleActions.PreTerminate.Exec).ShouldNot(BeNil()) + Expect(compDef.Spec.LifecycleActions.PreTerminate.Exec.Image).Should(Equal(testapps.AppImage(testapps.DefaultActionName, testapps.ReleaseID(r1)))) } Context("reconcile component version", func() { @@ -220,18 +231,19 @@ var _ = Describe("ComponentVersion Controller", func() { })).Should(Succeed()) }) - It("w/o container defined", func() { + It("w/o container or action defined", func() { By("update component version to add a non-exist app") compVersionKey := client.ObjectKeyFromObject(compVersionObj) Eventually(testapps.GetAndChangeObj(&testCtx, compVersionKey, func(compVersion *appsv1.ComponentVersion) { compVersion.Spec.Releases[0].Images["app-non-exist"] = "app-image-non-exist" })).Should(Succeed()) - By("checking the object unavailable") + By("checking the object available") Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(compVersionObj), func(g Gomega, cmpv *appsv1.ComponentVersion) { g.Expect(cmpv.Status.ObservedGeneration).Should(Equal(cmpv.GetGeneration())) - g.Expect(cmpv.Status.Phase).Should(Equal(appsv1.UnavailablePhase)) + // support to specify user-managed images + g.Expect(cmpv.Status.Phase).Should(Equal(appsv1.AvailablePhase)) })).Should(Succeed()) }) @@ -584,6 +596,8 @@ var _ = Describe("ComponentVersion Controller", func() { SetServiceVersion(testapps.ServiceVersion("v4")). SetRuntime(&corev1.Container{Name: testapps.AppName, Image: testapps.AppImage(testapps.AppName, testapps.ReleaseID(""))}). SetRuntime(&corev1.Container{Name: testapps.AppNameSamePrefix, Image: testapps.AppImage(testapps.AppNameSamePrefix, testapps.ReleaseID(""))}). + SetLifecycleAction(testapps.DefaultActionName, + &appsv1.Action{Exec: &appsv1.ExecAction{Image: testapps.AppImage(testapps.DefaultActionName, testapps.ReleaseID(""))}}). Create(&testCtx). GetObject() Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(compDefObj), diff --git a/deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml b/deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml index 9e224ef5808..97e945aa7f4 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml @@ -109,14 +109,18 @@ spec: images: additionalProperties: type: string - description: Images define the new images for different containers - within the release. + description: |- + Images define the new images for containers, actions or external applications within the release. + + + If an image is specified for a lifecycle action, the key should be the field name (case-insensitive) of + the action in the LifecycleActions struct. maxProperties: 128 minProperties: 1 type: object x-kubernetes-validations: - - message: Container name may not exceed maximum length of 32 - characters + - message: Container, action or external application name may + not exceed maximum length of 32 characters rule: self.all(key, size(key) <= 32) - message: Image name may not exceed maximum length of 256 characters rule: self.all(key, size(self[key]) <= 256) diff --git a/docs/developer_docs/api-reference/cluster.md b/docs/developer_docs/api-reference/cluster.md index 6c46a121c9d..7e555d7ac84 100644 --- a/docs/developer_docs/api-reference/cluster.md +++ b/docs/developer_docs/api-reference/cluster.md @@ -5770,7 +5770,9 @@ map[string]string
Images define the new images for different containers within the release.
+Images define the new images for containers, actions or external applications within the release.
+If an image is specified for a lifecycle action, the key should be the field name (case-insensitive) of +the action in the LifecycleActions struct.