Skip to content
This repository has been archived by the owner on Aug 19, 2024. It is now read-only.

Commit

Permalink
Update existing resources when CR changes
Browse files Browse the repository at this point in the history
  • Loading branch information
jianrongzhang89 committed Jan 5, 2024
1 parent 0307cc1 commit 159c97f
Show file tree
Hide file tree
Showing 14 changed files with 551 additions and 251 deletions.
5 changes: 5 additions & 0 deletions api/v1alpha1/backstage_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import (
const (
RuntimeConditionRunning string = "RuntimeRunning"
RuntimeConditionSynced string = "RuntimeSyncedWithConfig"
RouteSynced string = "RouteSynced"
LocalDbSynced string = "LocalDbSynced"
SynckOK string = "SyncOK"
SynckFailed string = "SyncFailed"
Deleted string = "Deleted"
EnvPostGresImage string = "RELATED_IMAGE_postgresql"
EnvBackstageImage string = "RELATED_IMAGE_backstage"
)
Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ metadata:
}
]
capabilities: Basic Install
createdAt: "2023-12-21T16:17:51Z"
createdAt: "2024-01-05T02:13:49Z"
operators.operatorframework.io/builder: operator-sdk-v1.33.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
name: backstage-operator.v0.0.1
Expand Down Expand Up @@ -206,7 +206,7 @@ spec:
value: quay.io/fedora/postgresql-15:latest
- name: RELATED_IMAGE_backstage
value: quay.io/janus-idp/backstage-showcase:next
image: quay.io/rhdh/backstage-operator:v0.0.1
image: quay.io/janus/operator:v0.0.1
livenessProbe:
httpGet:
path: /healthz
Expand Down
2 changes: 1 addition & 1 deletion config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: quay.io/rhdh/backstage-operator
newName: quay.io/janus/operator
newTag: v0.0.1

generatorOptions:
Expand Down
100 changes: 85 additions & 15 deletions controllers/backstage_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
return ctrl.Result{}, fmt.Errorf("failed to load backstage deployment from the cluster: %w", err)
}

// This update will make sure the status is always updated in case of any errors or successful result
defer func(bs *bs.Backstage) {
if err := r.Client.Status().Update(ctx, bs); err != nil {
if errors.IsConflict(err) {
lg.V(1).Info("Backstage object modified, retry syncing status", "Backstage Object", bs)
return
}
lg.Error(err, "Error updating the Backstage resource status", "Backstage Object", bs)
}
}(&backstage)

if pointer.BoolDeref(backstage.Spec.Database.EnableLocalDb, true) {

/* We use default strogeclass currently, and no PV is needed in that case.
Expand All @@ -108,41 +119,44 @@ func (r *BackstageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}
*/

err := r.applyLocalDbStatefulSet(ctx, backstage, req.Namespace)
err := r.reconcileLocalDbStatefulSet(ctx, backstage, req.Namespace)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to apply Database StatefulSet: %w", err)
setStatusCondition(&backstage, bs.LocalDbSynced, v1.ConditionFalse, bs.SynckFailed, fmt.Sprintf("failed to sync Database StatefulSet:%s", err.Error()))
return ctrl.Result{}, fmt.Errorf("failed to sync Database StatefulSet: %w", err)
}

err = r.applyLocalDbServices(ctx, backstage, req.Namespace)
err = r.reconcileLocalDbServices(ctx, backstage, req.Namespace)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to apply Database Service: %w", err)
setStatusCondition(&backstage, bs.LocalDbSynced, v1.ConditionFalse, bs.SynckFailed, fmt.Sprintf("failed to sync Database Services:%s", err.Error()))
return ctrl.Result{}, fmt.Errorf("failed to sync Database Service: %w", err)
}

setStatusCondition(&backstage, bs.LocalDbSynced, v1.ConditionTrue, bs.SynckOK, "")
} else if isLocalDbSynced(&backstage) { // EnableLocalDb is off but local db has been deployed. Clean up the deployed local db resources
if err := r.cleanupLocalDbResources(ctx, &backstage); err != nil {
setStatusCondition(&backstage, bs.LocalDbSynced, v1.ConditionFalse, bs.SynckFailed, fmt.Sprintf("failed to delete Database Services:%s", err.Error()))
return ctrl.Result{}, fmt.Errorf("failed to delete Database Service: %w", err)
}
setStatusCondition(&backstage, bs.LocalDbSynced, v1.ConditionFalse, bs.Deleted, "")
}

err := r.applyBackstageDeployment(ctx, backstage, req.Namespace)
err := r.reconcileBackstageDeployment(ctx, backstage, req.Namespace)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to apply Backstage Deployment: %w", err)
return ctrl.Result{}, fmt.Errorf("failed to reconcile Backstage Deployment: %w", err)
}

if err := r.applyBackstageService(ctx, backstage, req.Namespace); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to apply Backstage Service: %w", err)
if err := r.reconcileBackstageService(ctx, backstage, req.Namespace); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to reconcile Backstage Service: %w", err)
}

if r.IsOpenShift {
if err := r.applyBackstageRoute(ctx, backstage, req.Namespace); err != nil {
if err := r.reconcileBackstageRoute(ctx, &backstage, req.Namespace); err != nil {
return ctrl.Result{}, err
}
}

//TODO: it is just a placeholder for the time
r.setRunningStatus(ctx, &backstage, req.Namespace)
r.setSyncStatus(&backstage)
err = r.Status().Update(ctx, &backstage)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set status: %w", err)
//log.FromContext(ctx).Error(err, "unable to update backstage.status")
}

return ctrl.Result{}, nil
}
Expand Down Expand Up @@ -238,6 +252,62 @@ func (r *BackstageReconciler) setSyncStatus(backstage *bs.Backstage) {
})
}

// sets status condition
func setStatusCondition(backstage *bs.Backstage, condType string, status v1.ConditionStatus, reason, msg string) {
meta.SetStatusCondition(&backstage.Status.Conditions, v1.Condition{
Type: condType,
Status: status,
LastTransitionTime: v1.Time{},
Reason: reason,
Message: msg,
})
}

func isSynced(backstage *bs.Backstage) bool {
return isStatusConditionTrue(bs.RuntimeConditionSynced, backstage)
}

func isRouteSynced(backstage *bs.Backstage) bool {
return isStatusConditionTrue(bs.RouteSynced, backstage)
}

func isLocalDbSynced(backstage *bs.Backstage) bool {
return isStatusConditionTrue(bs.LocalDbSynced, backstage)
}

func isStatusConditionTrue(condType string, backstage *bs.Backstage) bool {
if cond := meta.FindStatusCondition(backstage.Status.Conditions, condType); cond != nil {
return cond.Status == v1.ConditionTrue
}
return false
}

// cleanupResource deletes the resource from the cluster if exists
func (r *BackstageReconciler) cleanupResource(ctx context.Context, obj client.Object, backstage *bs.Backstage) (bool, error) {
err := r.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, obj)
if err != nil {
if errors.IsNotFound(err) {
return false, nil // Nothing to delete
}
return false, err // For retry
}
ownedByCR := false
for _, ownerRef := range obj.GetOwnerReferences() {
if ownerRef.APIVersion == bs.GroupVersion.String() && ownerRef.Kind == "Backstage" && ownerRef.Name == backstage.Name {
ownedByCR = true
break
}
}
if !ownedByCR { // The object is not owned by the backstage CR
return false, nil
}
err = r.Delete(ctx, obj)
if err == nil {
return true, nil // Deleted
}
return false, err
}

// sets backstage-{Id} for labels and selectors
func setBackstageAppLabel(labels *map[string]string, backstage bs.Backstage) {
if *labels == nil {
Expand Down
161 changes: 159 additions & 2 deletions controllers/backstage_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ var _ = Describe("Backstage controller", func() {
err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, &backstage)
g.Expect(err).NotTo(HaveOccurred())
//TODO the status is under construction
g.Expect(len(backstage.Status.Conditions)).To(Equal(2))
g.Expect(isSynced(&backstage)).To(BeTrue())
}, time.Minute, time.Second).Should(Succeed())
}

Expand Down Expand Up @@ -347,6 +347,92 @@ var _ = Describe("Backstage controller", func() {

By("Checking the latest Status added to the Backstage instance")
verifyBackstageInstance(ctx)

By("Checking the localDb Sync Status in the Backstage instance")
Eventually(func(g Gomega) {
var backstage bsv1alpha1.Backstage
err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, &backstage)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(isLocalDbSynced(&backstage)).To(BeTrue())
}, time.Minute, time.Second).Should(Succeed())

By("Checking the localdb statefulset has been created")
Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("backstage-psql-%s", backstageName), Namespace: ns}, &appsv1.StatefulSet{})
g.Expect(err).To(Not(HaveOccurred()))
}, time.Minute, time.Second).Should(Succeed())

By("Checking the localdb services have been created")
Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("backstage-psql-%s", backstageName), Namespace: ns}, &corev1.Service{})
g.Expect(err).To(Not(HaveOccurred()))
err = k8sClient.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("backstage-psql-%s-hl", backstageName), Namespace: ns}, &corev1.Service{})
g.Expect(err).To(Not(HaveOccurred()))
}, time.Minute, time.Second).Should(Succeed())

By("Checking the localdb secret has been gnerated")
Eventually(func(g Gomega) {
err := k8sClient.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("backstage-psql-secret-%s", backstageName), Namespace: ns}, &corev1.Secret{})
g.Expect(err).To(Not(HaveOccurred()))
}, time.Minute, time.Second).Should(Succeed())

By("Updating custom resource by disabling local db")
var enableLocalDb bool = false
Eventually(func(g Gomega) {
toBeUpdated := &bsv1alpha1.Backstage{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, toBeUpdated)
g.Expect(err).To(Not(HaveOccurred()))
toBeUpdated.Spec.Database.EnableLocalDb = &enableLocalDb
toBeUpdated.Spec.Database.AuthSecretName = "existing-db-secret"
err = k8sClient.Update(ctx, toBeUpdated)
g.Expect(err).To(Not(HaveOccurred()))
}, time.Minute, time.Second).Should(Succeed())

By("Reconciling again after the custom resource update")
_, err = backstageReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns},
})
Expect(err).To(Not(HaveOccurred()))

By("Checking the localDb Sync Status has been updated in the Backstage instance")
Eventually(func(g Gomega) {
var backstage bsv1alpha1.Backstage
err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, &backstage)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(isLocalDbSynced(&backstage)).To(BeFalse())
}, time.Minute, time.Second).Should(Succeed())

By("Checking that the local db statefulset has been deleted")
Eventually(func(g Gomega) {
err := k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns, Name: fmt.Sprintf("backstage-psql-%s", backstage.Name)},
&appsv1.StatefulSet{})
g.Expect(err).Should(HaveOccurred())
g.Expect(errors.IsNotFound(err)).Should(BeTrue(), "Expected error to be a not-found one, but got %v", err)
}, time.Minute, time.Second).Should(Succeed())

By("Checking that the local db services have been deleted")
Eventually(func(g Gomega) {
err := k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns, Name: fmt.Sprintf("backstage-psql-%s", backstage.Name)},
&corev1.Service{})
g.Expect(err).Should(HaveOccurred())
g.Expect(errors.IsNotFound(err)).Should(BeTrue(), "Expected error to be a not-found one, but got %v", err)
err = k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns, Name: fmt.Sprintf("backstage-psql-%s-hl", backstage.Name)},
&corev1.Service{})
g.Expect(err).Should(HaveOccurred())
g.Expect(errors.IsNotFound(err)).Should(BeTrue(), "Expected error to be a not-found one, but got %v", err)
}, time.Minute, time.Second).Should(Succeed())

By("Checking that the local db secret has been deleted")
Eventually(func(g Gomega) {
err := k8sClient.Get(ctx,
types.NamespacedName{Namespace: ns, Name: fmt.Sprintf("backstage-psql-secret-%s", backstage.Name)},
&corev1.Secret{})
g.Expect(err).Should(HaveOccurred())
g.Expect(errors.IsNotFound(err)).Should(BeTrue(), "Expected error to be a not-found one, but got %v", err)
}, time.Minute, time.Second).Should(Succeed())
})
})

Expand Down Expand Up @@ -1157,6 +1243,45 @@ plugins: []

By("Checking the latest Status added to the Backstage instance")
verifyBackstageInstance(ctx)

By("Updating the custom resource with extra env vars")
Eventually(func(g Gomega) {
toBeUpdated := &bsv1alpha1.Backstage{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, toBeUpdated)
g.Expect(err).To(Not(HaveOccurred()))
toBeUpdated.Spec.Application.ExtraEnvs.Envs = []bsv1alpha1.Env{
{Name: "MY_ENV_VAR_3", Value: "value 30"},
}
err = k8sClient.Update(ctx, toBeUpdated)
g.Expect(err).To(Not(HaveOccurred()))
}, time.Minute, time.Second).Should(Succeed())

By("Checking extra envs in the custom resource are updated")
Eventually(func(g Gomega) {
found := &bsv1alpha1.Backstage{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found)
g.Expect(err).To(Not(HaveOccurred()))
g.Expect(found.Spec.Application.ExtraEnvs.Envs).Should(HaveLen(1))
g.Expect(found.Spec.Application.ExtraEnvs.Envs[0].Name).To(Equal("MY_ENV_VAR_3"))
}, time.Minute, time.Second).Should(Succeed())

By("Reconciling again after the custom resource update")
_, err = backstageReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns},
})
Expect(err).To(Not(HaveOccurred()))

By("Checking the Deployment's env variables are updated after the custom resource update")
Eventually(func(g Gomega) {
found := &appsv1.Deployment{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: fmt.Sprintf("backstage-%s", backstageName)}, found)
g.Expect(err).To(Not(HaveOccurred()))
mainCont := found.Spec.Template.Spec.Containers[0]
_, ok := findEnvVar(mainCont.Env, "MY_ENV_VAR_3")
Expect(ok).To(BeTrue(), "Env var MY_ENV_VAR_3 should be injected into the main container")
_, ok = findEnvVar(mainCont.Env, "MY_ENV_VAR_1")
Expect(ok).To(BeFalse(), "Env var MY_ENV_VAR_1 should have been removed from the main container")
}, time.Minute, time.Second).Should(Succeed())
})
})
})
Expand Down Expand Up @@ -1263,7 +1388,7 @@ plugins: []

When("setting the number of replicas", func() {
var nbReplicas int32 = 5

var nbReplicasUpdated int32 = 3
var backstage *bsv1alpha1.Backstage

BeforeEach(func() {
Expand Down Expand Up @@ -1302,6 +1427,38 @@ plugins: []

By("Checking the latest Status added to the Backstage instance")
verifyBackstageInstance(ctx)

By("Updating replicas in the custom resource")
Eventually(func(g Gomega) {
toBeUpdated := &bsv1alpha1.Backstage{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, toBeUpdated)
g.Expect(err).To(Not(HaveOccurred()))
toBeUpdated.Spec.Application.Replicas = &nbReplicasUpdated
err = k8sClient.Update(ctx, toBeUpdated)
g.Expect(err).To(Not(HaveOccurred()))
}, time.Minute, time.Second).Should(Succeed())

By("Checking replicas in the custom resource is updated")
Eventually(func(g Gomega) {
found := &bsv1alpha1.Backstage{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: backstageName, Namespace: ns}, found)
g.Expect(err).To(Not(HaveOccurred()))
g.Expect(found.Spec.Application.Replicas).Should(HaveValue(BeEquivalentTo(nbReplicasUpdated)))
}, time.Minute, time.Second).Should(Succeed())

By("Reconciling again after the custom resource update")
_, err = backstageReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Name: backstageName, Namespace: ns},
})
Expect(err).To(Not(HaveOccurred()))

By("Checking the Deployment's replicas is updated after replicas is updated in the custom resource")
Eventually(func(g Gomega) {
found := &appsv1.Deployment{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: fmt.Sprintf("backstage-%s", backstageName)}, found)
g.Expect(err).To(Not(HaveOccurred()))
g.Expect(found.Spec.Replicas).Should(HaveValue(BeEquivalentTo(nbReplicasUpdated)))
}, time.Minute, time.Second).Should(Succeed())
})
})

Expand Down
Loading

0 comments on commit 159c97f

Please sign in to comment.