Skip to content
Draft
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
11 changes: 11 additions & 0 deletions api/v1/clusterextension_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ type ClusterExtensionSpec struct {
//
// +optional
Config *ClusterExtensionConfig `json:"config,omitempty"`

// progressDeadlineMinutes is an optional field that defines the maximum period
// of time in minutes after which an installation should be considered failed and
// require manual intervention. This functionality is disabled when no value
// is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
//
// +kubebuilder:validation:Minimum:=10
// +kubebuilder:validation:Maximum:=720
// +optional
// <opcon:experimental>
ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"`
}

const SourceTypeCatalog = "Catalog"
Expand Down
11 changes: 11 additions & 0 deletions api/v1/clusterextensionrevision_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ type ClusterExtensionRevisionSpec struct {
// +listMapKey=name
// +optional
Phases []ClusterExtensionRevisionPhase `json:"phases,omitempty"`

// progressDeadlineMinutes is an optional field that defines the maximum period
// of time in minutes after which an installation should be considered failed and
// require manual intervention. This functionality is disabled when no value
// is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
//
// +kubebuilder:validation:Minimum:=10
// +kubebuilder:validation:Maximum:=720
// +optional
// <opcon:experimental>
ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"`
}

// ClusterExtensionRevisionLifecycleState specifies the lifecycle state of the ClusterExtensionRevision.
Expand Down
5 changes: 3 additions & 2 deletions api/v1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
ReasonDeprecated = "Deprecated"

// Common reasons
ReasonSucceeded = "Succeeded"
ReasonFailed = "Failed"
ReasonSucceeded = "Succeeded"
ReasonFailed = "Failed"
ReasonProgressDeadlineExceeded = "ProgressDeadlineExceeded"
)
1 change: 1 addition & 0 deletions docs/api-reference/olmv1-api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ _Appears in:_
| `source` _[SourceConfig](#sourceconfig)_ | source is required and selects the installation source of content for this ClusterExtension.<br />Set the sourceType field to perform the selection.<br />Catalog is currently the only implemented sourceType.<br />Setting sourceType to "Catalog" requires the catalog field to also be defined.<br />Below is a minimal example of a source definition (in yaml):<br />source:<br /> sourceType: Catalog<br /> catalog:<br /> packageName: example-package | | Required: \{\} <br /> |
| `install` _[ClusterExtensionInstallConfig](#clusterextensioninstallconfig)_ | install is optional and configures installation options for the ClusterExtension,<br />such as the pre-flight check configuration. | | |
| `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is optional and specifies bundle-specific configuration.<br />Configuration is bundle-specific and a bundle may provide a configuration schema.<br />When not specified, the default configuration of the resolved bundle is used.<br />config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide<br />a configuration schema the bundle is deemed to not be configurable. More information on how<br />to configure bundles can be found in the OLM documentation associated with your current OLM version. | | |
| `progressDeadlineMinutes` _integer_ | progressDeadlineMinutes is an optional field that defines the maximum period<br />of time in minutes after which an installation should be considered failed and<br />require manual intervention. This functionality is disabled when no value<br />is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).<br /><opcon:experimental> | | Maximum: 720 <br />Minimum: 10 <br /> |


#### ClusterExtensionStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,16 @@ spec:
x-kubernetes-validations:
- message: phases is immutable
rule: self == oldSelf || oldSelf.size() == 0
progressDeadlineMinutes:
description: |-
progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
format: int32
maximum: 720
minimum: 10
type: integer
revision:
description: |-
revision is a required, immutable sequence number representing a specific revision
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ spec:
rule: self == oldSelf
- message: namespace must be a valid DNS1123 label
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
progressDeadlineMinutes:
description: |-
progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
format: int32
maximum: 720
minimum: 10
type: integer
serviceAccount:
description: |-
serviceAccount specifies a ServiceAccount used to perform all interactions with the cluster
Expand Down
6 changes: 5 additions & 1 deletion internal/operator-controller/applier/boxcutter.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
annotations[labels.ServiceAccountNameKey] = ext.Spec.ServiceAccount.Name
annotations[labels.ServiceAccountNamespaceKey] = ext.Spec.Namespace

return &ocv1.ClusterExtensionRevision{
cer := &ocv1.ClusterExtensionRevision{
ObjectMeta: metav1.ObjectMeta{
Annotations: annotations,
Labels: map[string]string{
Expand All @@ -206,6 +206,10 @@ func (r *SimpleRevisionGenerator) buildClusterExtensionRevision(
Phases: PhaseSort(objects),
},
}
if p := ext.Spec.ProgressDeadlineMinutes; p > 0 {
cer.Spec.ProgressDeadlineMinutes = p
}
return cer
}

// BoxcutterStorageMigrator migrates ClusterExtensions from Helm-based storage to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ var ConditionReasons = []string{
ocv1.ReasonRetrying,
ocv1.ReasonAbsent,
ocv1.ReasonRollingOut,
ocv1.ReasonProgressDeadlineExceeded,
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"strings"
"sync"
"time"

appsv1 "k8s.io/api/apps/v1"
Expand Down Expand Up @@ -43,9 +44,10 @@ const (
// ClusterExtensionRevisionReconciler actions individual snapshots of ClusterExtensions,
// as part of the boxcutter integration.
type ClusterExtensionRevisionReconciler struct {
Client client.Client
RevisionEngineFactory RevisionEngineFactory
TrackingCache trackingCache
Client client.Client
RevisionEngineFactory RevisionEngineFactory
TrackingCache trackingCache
progressionRequeueOnce sync.Map
}

type trackingCache interface {
Expand Down Expand Up @@ -74,6 +76,17 @@ func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req
reconciledRev := existingRev.DeepCopy()
res, reconcileErr := c.reconcile(ctx, reconciledRev)

if pd := existingRev.Spec.ProgressDeadlineMinutes; pd > 0 {
if cnd := meta.FindStatusCondition(reconciledRev.Status.Conditions, ocv1.ClusterExtensionRevisionTypeProgressing); cnd != nil && cnd.Status == metav1.ConditionTrue && cnd.Reason != ocv1.ReasonSucceeded {
timeout := time.Duration(pd) * time.Minute
if time.Since(existingRev.CreationTimestamp.Time) > timeout {
markAsNotProgressing(reconciledRev, ocv1.ReasonProgressDeadlineExceeded, fmt.Sprintf("Revision has not rolled out for %d minutes.", pd))
// reset any errors that may have occurred during reconciliation and stop any further reconciliation due to the reached timeout
reconcileErr = nil
res = ctrl.Result{}
}
}
}
// Do checks before any Update()s, as Update() may modify the resource structure!
updateStatus := !equality.Semantic.DeepEqual(existingRev.Status, reconciledRev.Status)

Expand All @@ -92,6 +105,10 @@ func (c *ClusterExtensionRevisionReconciler) Reconcile(ctx context.Context, req
}
}

if _, found := c.progressionRequeueOnce.Load(existingRev.GetUID()); !found && reconcileErr == nil && existingRev.Spec.ProgressDeadlineMinutes > 0 {
c.progressionRequeueOnce.Store(existingRev.GetUID(), true)
res = ctrl.Result{RequeueAfter: time.Duration(existingRev.Spec.ProgressDeadlineMinutes) * time.Minute}
}
return res, reconcileErr
}

Expand Down
20 changes: 20 additions & 0 deletions manifests/experimental-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,16 @@ spec:
x-kubernetes-validations:
- message: phases is immutable
rule: self == oldSelf || oldSelf.size() == 0
progressDeadlineMinutes:
description: |-
progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
format: int32
maximum: 720
minimum: 10
type: integer
revision:
description: |-
revision is a required, immutable sequence number representing a specific revision
Expand Down Expand Up @@ -1052,6 +1062,16 @@ spec:
rule: self == oldSelf
- message: namespace must be a valid DNS1123 label
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
progressDeadlineMinutes:
description: |-
progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
format: int32
maximum: 720
minimum: 10
type: integer
serviceAccount:
description: |-
serviceAccount specifies a ServiceAccount used to perform all interactions with the cluster
Expand Down
20 changes: 20 additions & 0 deletions manifests/experimental.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,16 @@ spec:
x-kubernetes-validations:
- message: phases is immutable
rule: self == oldSelf || oldSelf.size() == 0
progressDeadlineMinutes:
description: |-
progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
format: int32
maximum: 720
minimum: 10
type: integer
revision:
description: |-
revision is a required, immutable sequence number representing a specific revision
Expand Down Expand Up @@ -1013,6 +1023,16 @@ spec:
rule: self == oldSelf
- message: namespace must be a valid DNS1123 label
rule: self.matches("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
progressDeadlineMinutes:
description: |-
progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
format: int32
maximum: 720
minimum: 10
type: integer
serviceAccount:
description: |-
serviceAccount specifies a ServiceAccount used to perform all interactions with the cluster
Expand Down
28 changes: 28 additions & 0 deletions test/e2e/features/install.feature
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,31 @@ Feature: Install ClusterExtension
valid: true
mutate: true
"""

@foo
@BoxcutterRuntime
Scenario: Progression deadline
Given min value for ClusterExtension .spec.progressDeadlineMinutes is set to 1
And min value for ClusterExtensionRevision .spec.progressDeadlineMinutes is set to 1
When ClusterExtension is applied
"""
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: ${NAME}
spec:
namespace: ${TEST_NAMESPACE}
progressDeadlineMinutes: 1
serviceAccount:
name: olm-sa
source:
sourceType: Catalog
catalog:
packageName: test
version: 1.0.3
selector:
matchLabels:
"olm.operatorframework.io/metadata.name": test-catalog
"""
Then ClusterExtensionRevision "${NAME}-1" reports Progressing as False with Reason ProgressDeadlineExceeded
And ClusterExtension reports Progressing as False with Reason ProgressDeadlineExceeded
29 changes: 29 additions & 0 deletions test/e2e/steps/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ func RegisterSteps(sc *godog.ScenarioContext) {

sc.Step(`^(?i)operator "([^"]+)" target namespace is "([^"]+)"$`, OperatorTargetNamespace)
sc.Step(`^(?i)Prometheus metrics are returned in the response$`, PrometheusMetricsAreReturned)

sc.Step(`^(?i)min value for (ClusterExtension|ClusterExtensionRevision) ((?:\.[a-zA-Z]+)+) is set to (\d+)$`, SetCRDFieldMinValue)
}

func init() {
Expand Down Expand Up @@ -730,3 +732,30 @@ func MarkTestOperatorNotReady(ctx context.Context, state string) error {
_, err = k8sClient("exec", podName, "-n", sc.namespace, "--", op, "/var/www/ready")
return err
}

// SetCRDFieldMinValue patches a CRD to set the minimum value for a field.
// jsonPath is in the format ".spec.fieldName" and gets converted to the CRD schema path.
func SetCRDFieldMinValue(_ context.Context, resourceType, jsonPath string, minValue int) error {
var crdName string
switch resourceType {
case "ClusterExtension":
crdName = "clusterextensions.olm.operatorframework.io"
case "ClusterExtensionRevision":
crdName = "clusterextensionrevisions.olm.operatorframework.io"
default:
return fmt.Errorf("unsupported resource type: %s", resourceType)
}

// Convert JSON path like ".spec.progressDeadlineMinutes" to CRD schema path
// e.g., ".spec.progressDeadlineMinutes" -> "properties/spec/properties/progressDeadlineMinutes"
parts := strings.Split(strings.TrimPrefix(jsonPath, "."), ".")
schemaParts := make([]string, 0, 2*len(parts))
for _, part := range parts {
schemaParts = append(schemaParts, "properties", part)
}
patchPath := fmt.Sprintf("/spec/versions/0/schema/openAPIV3Schema/%s/minimum", strings.Join(schemaParts, "/"))

patch := fmt.Sprintf(`[{"op": "replace", "path": "%s", "value": %d}]`, patchPath, minValue)
_, err := k8sClient("patch", "crd", crdName, "--type=json", "-p", patch)
return err
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: wrong-test-configmap
wrongfield:
name: "test-configmap"
Loading
Loading