Skip to content

Commit

Permalink
feat: cmpv supports images of the lifecycle action and the user-manag…
Browse files Browse the repository at this point in the history
…ed (#8238)
  • Loading branch information
leon-inf authored Oct 9, 2024
1 parent 038fc44 commit 708cb8d
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 71 deletions.
7 changes: 5 additions & 2 deletions apis/apps/v1/componentversion_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
12 changes: 8 additions & 4 deletions config/crd/bases/apps.kubeblocks.io_componentversions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 40 additions & 13 deletions controllers/apps/componentversion_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,31 +323,58 @@ 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
}
}
}
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 {
Expand Down
20 changes: 17 additions & 3 deletions controllers/apps/componentversion_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")),
},
},
{
Expand All @@ -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")),
},
},
{
Expand All @@ -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")),
},
},
{
Expand All @@ -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")),
},
},
},
Expand All @@ -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() {
Expand Down Expand Up @@ -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())
})

Expand Down Expand Up @@ -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),
Expand Down
12 changes: 8 additions & 4 deletions deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion docs/developer_docs/api-reference/cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -5770,7 +5770,9 @@ map[string]string
</em>
</td>
<td>
<p>Images define the new images for different containers within the release.</p>
<p>Images define the new images for containers, actions or external applications within the release.</p>
<p>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.</p>
</td>
</tr>
</tbody>
Expand Down
Loading

0 comments on commit 708cb8d

Please sign in to comment.