Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions api/current-types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,24 @@ type (

// Condition constants
const (
BackstageConditionTypeDeployed BackstageConditionType = bsv1.BackstageConditionTypeDeployed
BackstageConditionReasonDeployed BackstageConditionReason = bsv1.BackstageConditionReasonDeployed
BackstageConditionReasonFailed BackstageConditionReason = bsv1.BackstageConditionReasonFailed
BackstageConditionReasonInProgress BackstageConditionReason = bsv1.BackstageConditionReasonInProgress
BackstageConditionReasonIdled BackstageConditionReason = bsv1.BackstageConditionReasonIdled
Comment thread
rm3l marked this conversation as resolved.
// Condition types
BackstageConditionTypeDeployed BackstageConditionType = bsv1.BackstageConditionTypeDeployed
BackstageConditionTypeRuntime BackstageConditionType = bsv1.BackstageConditionTypeRuntime
BackstageConditionTypeConfig BackstageConditionType = bsv1.BackstageConditionTypeConfig

// Deployed condition reasons
BackstageConditionReasonDeployed BackstageConditionReason = bsv1.BackstageConditionReasonDeployed
BackstageConditionReasonFailed BackstageConditionReason = bsv1.BackstageConditionReasonFailed
BackstageConditionReasonInProgress BackstageConditionReason = bsv1.BackstageConditionReasonInProgress
BackstageConditionReasonRolloutStalled BackstageConditionReason = bsv1.BackstageConditionReasonRolloutStalled

// Runtime condition reasons
BackstageConditionReasonRunning BackstageConditionReason = bsv1.BackstageConditionReasonRunning
BackstageConditionReasonContainerFailed BackstageConditionReason = bsv1.BackstageConditionReasonContainerFailed
BackstageConditionReasonPending BackstageConditionReason = bsv1.BackstageConditionReasonPending

// Config condition reasons
BackstageConditionReasonInvalid BackstageConditionReason = bsv1.BackstageConditionReasonInvalid
)

// AddToScheme adds the current API version's types to the scheme.
Expand Down
25 changes: 20 additions & 5 deletions api/v1alpha5/backstage_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,24 @@ type BackstageConditionReason string
type BackstageConditionType string

const (
// Condition types
BackstageConditionTypeDeployed BackstageConditionType = "Deployed"

BackstageConditionReasonDeployed BackstageConditionReason = "Deployed"
BackstageConditionReasonFailed BackstageConditionReason = "DeployFailed"
BackstageConditionReasonInProgress BackstageConditionReason = "DeployInProgress"
BackstageConditionReasonIdled BackstageConditionReason = "Idled"
BackstageConditionTypeRuntime BackstageConditionType = "Runtime"
BackstageConditionTypeConfig BackstageConditionType = "Config"

// Deployed condition reasons
BackstageConditionReasonDeployed BackstageConditionReason = "Deployed"
BackstageConditionReasonFailed BackstageConditionReason = "DeployFailed"
BackstageConditionReasonInProgress BackstageConditionReason = "DeployInProgress"
BackstageConditionReasonRolloutStalled BackstageConditionReason = "RolloutStalled"

// Runtime condition reasons
BackstageConditionReasonRunning BackstageConditionReason = "Running"
BackstageConditionReasonContainerFailed BackstageConditionReason = "ContainerFailed"
BackstageConditionReasonPending BackstageConditionReason = "Pending"

// Config condition reasons
BackstageConditionReasonInvalid BackstageConditionReason = "Invalid"
)

// BackstageSpec defines the desired state of Backstage
Expand Down Expand Up @@ -267,6 +279,9 @@ type BackstageStatus struct {
// Conditions is the list of conditions describing the state of the runtime
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Plugins is the list of enabled dynamic plugin names, sorted alphabetically
// +optional
Plugins []string `json:"plugins,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha5/zz_generated.deepcopy.go

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

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

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

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

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

6 changes: 6 additions & 0 deletions bundle/rhdh/manifests/rhdh.redhat.com_backstages.yaml

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

6 changes: 6 additions & 0 deletions config/crd/bases/rhdh.redhat.com_backstages.yaml

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

1 change: 0 additions & 1 deletion config/profile/rhdh/default-config/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ spec:
value: /opt/app-root/src/.npmrc.dynamic-plugins/.npmrc
- name: MAX_ENTRY_SIZE
value: "40000000"
# CATALOG_INDEX_IMAGE will be replaced by the value of the `RELATED_IMAGE_catalog_index` env var, if set
- name: CATALOG_INDEX_IMAGE
value: "quay.io/rhdh/plugin-catalog-index:next"
- name: CATALOG_ENTITIES_EXTRACT_DIR
Expand Down
1 change: 1 addition & 0 deletions config/rbac/role.yaml

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

7 changes: 7 additions & 0 deletions dist/backstage.io/install.yaml

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

8 changes: 7 additions & 1 deletion dist/rhdh/install.yaml

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

12 changes: 6 additions & 6 deletions docs/admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,14 @@ kubectl annotate backstage <cr-name> rhdh.redhat.com/idle=true

After reconciliation, the status condition will show:

```
Type: Deployed
Status: False
Reason: Idled
Message: Instance is idled
```yaml
- type: Deployed
status: "True"
reason: Deployed
message: "0/0 replicas ready (Idled)"
```

> **Note for CI and monitoring scripts:** An idled instance reports `Deployed=False` with `Reason=Idled`. Scripts that wait for `Deployed=True` should check the `Reason` field to distinguish an intentionally idled instance from a deployment failure. To ensure readiness checks succeed, remove the `rhdh.redhat.com/idle` annotation before waiting for deployment.
> **Note:** To determine if an instance is idled, check the `rhdh.redhat.com/idle` annotation on the Backstage CR. Absence of this annotation means the instance is not idled.

### Waking an instance

Expand Down
95 changes: 90 additions & 5 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,94 @@ Kubernetes doesn’t allow you to change environment variables after a Pod has b

## Status

Backstage Custom Resource contains **Deployed** condition in the Status field.
It is updated by the Operator and can have the following values:
- **DeployInProgress** - Backstage Deployment is not available yet. The current state of Deployment can be seen in the message field
- **Deployed** - Backstage Deployment is being created and application is available
- **DeployFailed** - Backstage Deployment creation failed. The actual error can be seen in the message field
The Backstage Custom Resource contains status conditions that reflect the current state of the deployment.
The Operator maintains two primary condition types: **Deployed** and **Runtime**.

### Deployed Condition

The **Deployed** condition reflects the Deployment (or StatefulSet) level status:

| Reason | Status | Description |
|--------|--------|-------------|
| `Deployed` | True | All replicas are ready and running the current spec. Message shows "X/X replicas ready" |
| `DeployInProgress` | False | Rollout in progress. Message shows "X/X replicas ready, X/X updated" |
| `DeployFailed` | False | Deployment failed. Message contains the error details |
| `RolloutStalled` | False | Rolling update stalled - old pods healthy, new pods failing. Message shows "X/X replicas ready, X/X updated (rollout stalled)" |

**RolloutStalled:** This reason indicates a failed rolling update where old pods continue serving traffic while new pods fail to start. This occurs when Kubernetes' `progressDeadlineSeconds` (default: 600s) is exceeded. To recover, either fix the issue in the Backstage CR or run `kubectl rollout undo deployment/backstage-<cr-name>`.

**Idled instances:** When the `rhdh.redhat.com/idle: true` annotation is set, the deployment scales to 0 replicas. The Deployed condition shows `Deployed` with message "0/0 replicas ready (Idled)".

### Runtime Condition

The **Runtime** condition provides Pod and container-level status, offering more immediate feedback about application health:

| Reason | Status | Description |
|--------|--------|-------------|
| `Running` | True | All containers are running and healthy |
| `ContainerFailed` | False | A container has failed. See table below for specific scenarios |
| `Pending` | False | Containers not yet ready (e.g., still starting, init container running) |

**ContainerFailed scenarios:**

| Scenario | Example Message |
|----------|-----------------|
| Image pull failure | `container "backstage-backend": ImagePullBackOff` |
| Crash loop | `container "backstage-backend" crashed (restart #3), last exit code 1: connection refused` |
| Container terminated | `container "backstage-backend" failed with exit code 137: OOMKilled` |
| Init container failure | `init container "install-plugins" failed with exit code 1: plugin download failed` |

**Note:** Crash loop detection is smart about recovery. If a container has restart history but is currently Ready, it is considered healthy and will not be reported as crashed.

### Example Status

```yaml
status:
conditions:
- type: Deployed
status: "True"
reason: Deployed
message: "1/1 replicas ready"
- type: Runtime
status: "True"
reason: Running
message: ""
plugins:
- "@backstage/plugin-catalog"
- "@backstage/plugin-techdocs"
```

### Rollout Stalled Example

When a rolling update fails (e.g., bad image, missing config):

```yaml
status:
conditions:
- type: Deployed
status: "False"
reason: RolloutStalled
message: "2/2 replicas ready, 0/2 updated (rollout stalled)"
- type: Runtime
status: "False"
reason: ContainerFailed
message: 'container "backstage": ImagePullBackOff'
```

### Idled Example

When idled via annotation `rhdh.redhat.com/idle: true`:

```yaml
status:
conditions:
- type: Deployed
status: "True"
reason: Deployed
message: "0/0 replicas ready (Idled)"
```

### Plugins Status

When the deployment is healthy (both Deployed and Runtime conditions are True), the status includes a list of enabled dynamic plugins in the `plugins` field. This field is cleared when the deployment is not healthy or idled.

13 changes: 7 additions & 6 deletions integration_tests/default-config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"time"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/redhat-developer/rhdh-operator/pkg/model"
Expand Down Expand Up @@ -172,18 +173,18 @@ var _ = When("create default backstage", func() {
bs := &api.Backstage{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backstageName}, bs)
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(bs.Status.Conditions).To(HaveLen(1))
g.Expect(bs.Status.Conditions[0].Reason).To(Equal("DeployInProgress"))
g.Expect(bs.Status.Conditions[0].Status).To(Equal(metav1.ConditionFalse))
g.Expect(bs.Status.Conditions).To(HaveLen(2))
g.Expect(meta.FindStatusCondition(bs.Status.Conditions, string(api.BackstageConditionTypeDeployed)).Reason).To(Equal("DeployInProgress"))
g.Expect(meta.FindStatusCondition(bs.Status.Conditions, string(api.BackstageConditionTypeDeployed)).Status).To(Equal(metav1.ConditionFalse))
}, time.Minute, time.Second).Should(Succeed())

Eventually(func(g Gomega) {
bs := &api.Backstage{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: backstageName}, bs)
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(bs.Status.Conditions).To(HaveLen(1))
g.Expect(bs.Status.Conditions[0].Reason).To(Equal("Deployed"))
g.Expect(bs.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
g.Expect(bs.Status.Conditions).To(HaveLen(2))
g.Expect(meta.FindStatusCondition(bs.Status.Conditions, string(api.BackstageConditionTypeDeployed)).Reason).To(Equal("Deployed"))
g.Expect(meta.FindStatusCondition(bs.Status.Conditions, string(api.BackstageConditionTypeDeployed)).Status).To(Equal(metav1.ConditionTrue))
}, 3*time.Minute, time.Second).Should(Succeed())

})
Expand Down
2 changes: 2 additions & 0 deletions integration_tests/rhdh-config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ var _ = When("create default rhdh", func() {

}, 10*time.Second, time.Second).Should(Succeed())

deleteNamespace(ctx, ns)
})

It("replaces .npmrc", func() {
Expand Down Expand Up @@ -307,6 +308,7 @@ var _ = When("create default rhdh", func() {

}, 10*time.Second, time.Second).Should(Succeed())

deleteNamespace(ctx, ns)
})

It("creates rhdh with default Lightspeed flavour", func() {
Expand Down
Loading
Loading