Skip to content

Commit 586ee40

Browse files
Merge pull request #579 from tmshort/sync-2023-10-05-no-steve
Sync 2023 10 05 no steve
2 parents 8d6f8c4 + 3ff5495 commit 586ee40

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1669
-832
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ require (
1313
github.com/mikefarah/yq/v3 v3.0.0-20201202084205-8846255d1c37
1414
github.com/onsi/ginkgo/v2 v2.9.5
1515
github.com/openshift/api v3.9.0+incompatible
16-
github.com/operator-framework/api v0.17.8-0.20230908201838-28c6773d2b74
16+
github.com/operator-framework/api v0.17.8-0.20230929142219-7961b0208d99
1717
github.com/operator-framework/operator-lifecycle-manager v0.0.0-00010101000000-000000000000
1818
github.com/operator-framework/operator-registry v1.29.0
1919
github.com/sirupsen/logrus v1.9.2

manifests/0000_50_olm_00-olmconfigs.crd.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ spec:
4545
disableCopiedCSVs:
4646
description: DisableCopiedCSVs is used to disable OLM's "Copied CSV" feature for operators installed at the cluster scope, where a cluster scoped operator is one that has been installed in an OperatorGroup that targets all namespaces. When reenabled, OLM will recreate the "Copied CSVs" for each cluster scoped operator.
4747
type: boolean
48+
packageServerSyncInterval:
49+
description: PackageServerSyncInterval is used to define the sync interval for packagerserver pods. Packageserver pods periodically check the status of CatalogSources; this specifies the period using duration format (e.g. "60m"). For this parameter, only hours ("h"), minutes ("m"), and seconds ("s") may be specified. When not specified, the period defaults to the value specified within the packageserver.
50+
type: string
51+
pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))+$
4852
status:
4953
description: OLMConfigStatus is the status for an OLMConfig resource.
5054
type: object

staging/api/crds/operators.coreos.com_olmconfigs.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ spec:
4343
disableCopiedCSVs:
4444
description: DisableCopiedCSVs is used to disable OLM's "Copied CSV" feature for operators installed at the cluster scope, where a cluster scoped operator is one that has been installed in an OperatorGroup that targets all namespaces. When reenabled, OLM will recreate the "Copied CSVs" for each cluster scoped operator.
4545
type: boolean
46+
packageServerSyncInterval:
47+
description: PackageServerSyncInterval is used to define the sync interval for packagerserver pods. Packageserver pods periodically check the status of CatalogSources; this specifies the period using duration format (e.g. "60m"). For this parameter, only hours ("h"), minutes ("m"), and seconds ("s") may be specified. When not specified, the period defaults to the value specified within the packageserver.
48+
type: string
49+
pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))+$
4650
status:
4751
description: OLMConfigStatus is the status for an OLMConfig resource.
4852
type: object

staging/api/crds/zz_defs.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

staging/api/pkg/operators/v1/olmconfig_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,84 @@ package v1
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/stretchr/testify/require"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
79
)
810

911
func boolPointer(in bool) *bool {
1012
return &in
1113
}
14+
func TestPackageServerSyncInterval(t *testing.T) {
15+
five := time.Minute * 5
16+
one := time.Second * 60
1217

18+
fiveParsed, err := time.ParseDuration("5m")
19+
require.NoError(t, err)
20+
21+
oneParsed, err := time.ParseDuration("60s")
22+
require.NoError(t, err)
23+
24+
tests := []struct {
25+
description string
26+
olmConfig *OLMConfig
27+
expected *time.Duration
28+
}{
29+
{
30+
description: "NilConfig",
31+
olmConfig: nil,
32+
expected: nil,
33+
},
34+
{
35+
description: "MissingSpec",
36+
olmConfig: &OLMConfig{},
37+
expected: nil,
38+
},
39+
{
40+
description: "MissingFeatures",
41+
olmConfig: &OLMConfig{
42+
Spec: OLMConfigSpec{},
43+
},
44+
expected: nil,
45+
},
46+
{
47+
description: "MissingPackageServerInterval",
48+
olmConfig: &OLMConfig{
49+
Spec: OLMConfigSpec{},
50+
},
51+
expected: nil,
52+
},
53+
{
54+
description: "PackageServerInterval5m",
55+
olmConfig: &OLMConfig{
56+
Spec: OLMConfigSpec{
57+
Features: &Features{
58+
PackageServerSyncInterval: &metav1.Duration{Duration: fiveParsed},
59+
},
60+
},
61+
},
62+
expected: &five,
63+
},
64+
{
65+
description: "PackageServerInterval60s",
66+
olmConfig: &OLMConfig{
67+
Spec: OLMConfigSpec{
68+
Features: &Features{
69+
PackageServerSyncInterval: &metav1.Duration{Duration: oneParsed},
70+
},
71+
},
72+
},
73+
expected: &one,
74+
},
75+
}
76+
77+
for _, tt := range tests {
78+
t.Run(tt.description, func(t *testing.T) {
79+
require.EqualValues(t, tt.expected, tt.olmConfig.PackageServerSyncInterval())
80+
})
81+
}
82+
}
1383
func TestCopiedCSVsAreEnabled(t *testing.T) {
1484
tests := []struct {
1585
description string

staging/api/pkg/operators/v1/olmconfig_types.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package v1
22

33
import (
4+
"time"
5+
46
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
57
)
68

@@ -23,6 +25,16 @@ type Features struct {
2325
// When reenabled, OLM will recreate the "Copied CSVs" for each
2426
// cluster scoped operator.
2527
DisableCopiedCSVs *bool `json:"disableCopiedCSVs,omitempty"`
28+
// PackageServerSyncInterval is used to define the sync interval for
29+
// packagerserver pods. Packageserver pods periodically check the
30+
// status of CatalogSources; this specifies the period using duration
31+
// format (e.g. "60m"). For this parameter, only hours ("h"), minutes
32+
// ("m"), and seconds ("s") may be specified. When not specified, the
33+
// period defaults to the value specified within the packageserver.
34+
// +optional
35+
// +kubebuilder:validation:Type=string
36+
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(s|m|h))+$"
37+
PackageServerSyncInterval *metav1.Duration `json:"packageServerSyncInterval,omitempty"`
2638
}
2739

2840
// OLMConfigStatus is the status for an OLMConfig resource.
@@ -69,3 +81,10 @@ func (config *OLMConfig) CopiedCSVsAreEnabled() bool {
6981

7082
return !*config.Spec.Features.DisableCopiedCSVs
7183
}
84+
85+
func (config *OLMConfig) PackageServerSyncInterval() *time.Duration {
86+
if config == nil || config.Spec.Features == nil || config.Spec.Features.PackageServerSyncInterval == nil {
87+
return nil
88+
}
89+
return &config.Spec.Features.PackageServerSyncInterval.Duration
90+
}

staging/api/pkg/operators/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

staging/operator-lifecycle-manager/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ Learn more about the components used by OLM by reading about the [architecture]
8484

8585
OLM standardizes interactions with operators by requiring that the interface to an operator be via the Kubernetes API. Because we expect users to define the interfaces to their applications, OLM currently uses CRDs to define the Kubernetes API interactions.
8686

87-
Examples: [EtcdCluster CRD](https://github.com/redhat-openshift-ecosystem/community-operators-prod/blob/main/operators/etcd/0.9.4/etcdclusters.etcd.database.coreos.com.crd.yaml),
88-
[EtcdBackup CRD](https://github.com/redhat-openshift-ecosystem/community-operators-prod/blob/main/operators/etcd/0.9.4/etcdbackups.etcd.database.coreos.com.crd.yaml)
87+
Examples: [EtcdCluster CRD](https://github.com/redhat-openshift-ecosystem/community-operators-prod/blob/main/operators/etcd/0.9.4/manifests/etcdclusters.etcd.database.coreos.com.crd.yaml),
88+
[EtcdBackup CRD](https://github.com/redhat-openshift-ecosystem/community-operators-prod/blob/main/operators/etcd/0.9.4/manifests/etcdbackups.etcd.database.coreos.com.crd.yaml)
8989

9090
## Descriptors
9191

staging/operator-lifecycle-manager/deploy/chart/crds/0000_50_olm_00-olmconfigs.crd.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ spec:
4343
disableCopiedCSVs:
4444
description: DisableCopiedCSVs is used to disable OLM's "Copied CSV" feature for operators installed at the cluster scope, where a cluster scoped operator is one that has been installed in an OperatorGroup that targets all namespaces. When reenabled, OLM will recreate the "Copied CSVs" for each cluster scoped operator.
4545
type: boolean
46+
packageServerSyncInterval:
47+
description: PackageServerSyncInterval is used to define the sync interval for packagerserver pods. Packageserver pods periodically check the status of CatalogSources; this specifies the period using duration format (e.g. "60m"). For this parameter, only hours ("h"), minutes ("m"), and seconds ("s") may be specified. When not specified, the period defaults to the value specified within the packageserver.
48+
type: string
49+
pattern: ^([0-9]+(\.[0-9]+)?(s|m|h))+$
4650
status:
4751
description: OLMConfigStatus is the status for an OLMConfig resource.
4852
type: object

staging/operator-lifecycle-manager/deploy/chart/templates/_packageserver.deployment-spec.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ spec:
4747
{{- if .Values.debug }}
4848
- --debug
4949
{{- end }}
50+
{{- if .Values.package.interval }}
51+
- --interval
52+
- {{ .Values.package.interval }}
53+
{{- end }}
5054
{{- if .Values.package.commandArgs }}
5155
- {{ .Values.package.commandArgs }}
5256
{{- end }}

staging/operator-lifecycle-manager/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/onsi/gomega v1.27.7
2626
github.com/openshift/api v3.9.0+incompatible
2727
github.com/openshift/client-go v0.0.0-20220525160904-9e1acff93e4a
28-
github.com/operator-framework/api v0.17.8-0.20230908201838-28c6773d2b74
28+
github.com/operator-framework/api v0.17.8-0.20230929142219-7961b0208d99
2929
github.com/operator-framework/operator-registry v1.29.0
3030
github.com/otiai10/copy v1.12.0
3131
github.com/pkg/errors v0.9.1

staging/operator-lifecycle-manager/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,8 @@ github.com/openshift/api v0.0.0-20221021112143-4226c2167e40 h1:PxjGCA72RtsdHWToZ
604604
github.com/openshift/api v0.0.0-20221021112143-4226c2167e40/go.mod h1:aQ6LDasvHMvHZXqLHnX2GRmnfTWCF/iIwz8EMTTIE9A=
605605
github.com/openshift/client-go v0.0.0-20221019143426-16aed247da5c h1:CV76yFOTXmq9VciBR3Bve5ZWzSxdft7gaMVB3kS0rwg=
606606
github.com/openshift/client-go v0.0.0-20221019143426-16aed247da5c/go.mod h1:lFMO8mLHXWFzSdYvGNo8ivF9SfF6zInA8ZGw4phRnUE=
607-
github.com/operator-framework/api v0.17.8-0.20230908201838-28c6773d2b74 h1:BNzxQqrfGRaEuw5SliqTFvloLE76L1MAo/uzbszzrPw=
608-
github.com/operator-framework/api v0.17.8-0.20230908201838-28c6773d2b74/go.mod h1:Wbg136l1Po6zqG2QcTN1QZ8dbT4BQvNlQDM9tmQYvz0=
607+
github.com/operator-framework/api v0.17.8-0.20230929142219-7961b0208d99 h1:0x4FfGvKIEmpXnhqX9OumEnvJWn51zUVwvFulh17tu4=
608+
github.com/operator-framework/api v0.17.8-0.20230929142219-7961b0208d99/go.mod h1:Wbg136l1Po6zqG2QcTN1QZ8dbT4BQvNlQDM9tmQYvz0=
609609
github.com/operator-framework/operator-registry v1.29.0 h1:HMmVTiuOAGoHLzYqR9Lr2QSOqbVzA50++ojNl2mu9f4=
610610
github.com/operator-framework/operator-registry v1.29.0/go.mod h1:4rVQu/cOuCtVt3JzKsAmwyq2lsiu9uPaH9nYNfnqj9o=
611611
github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY=

staging/operator-lifecycle-manager/pkg/controller/bundle/bundle_unpacker.go

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"crypto/sha256"
66
"fmt"
7+
"sort"
78
"strings"
89
"time"
910

@@ -18,6 +19,7 @@ import (
1819
"k8s.io/apimachinery/pkg/api/resource"
1920
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2021
k8slabels "k8s.io/apimachinery/pkg/labels"
22+
"k8s.io/apiserver/pkg/storage/names"
2123
"k8s.io/client-go/kubernetes"
2224
listersbatchv1 "k8s.io/client-go/listers/batch/v1"
2325
listerscorev1 "k8s.io/client-go/listers/core/v1"
@@ -41,6 +43,13 @@ const (
4143
// e.g 1m30s
4244
BundleUnpackTimeoutAnnotationKey = "operatorframework.io/bundle-unpack-timeout"
4345
BundleUnpackPodLabel = "job-name"
46+
47+
// BundleUnpackRetryMinimumIntervalAnnotationKey sets a minimum interval to wait before
48+
// attempting to recreate a failed unpack job for a bundle.
49+
BundleUnpackRetryMinimumIntervalAnnotationKey = "operatorframework.io/bundle-unpack-min-retry-interval"
50+
51+
// bundleUnpackRefLabel is used to filter for all unpack jobs for a specific bundle.
52+
bundleUnpackRefLabel = "operatorframework.io/bundle-unpack-ref"
4453
)
4554

4655
type BundleUnpackResult struct {
@@ -89,6 +98,7 @@ func (c *ConfigMapUnpacker) job(cmRef *corev1.ObjectReference, bundlePath string
8998
ObjectMeta: metav1.ObjectMeta{
9099
Labels: map[string]string{
91100
install.OLMManagedLabelKey: install.OLMManagedLabelValue,
101+
bundleUnpackRefLabel: cmRef.Name,
92102
},
93103
},
94104
Spec: batchv1.JobSpec{
@@ -287,7 +297,7 @@ func (c *ConfigMapUnpacker) job(cmRef *corev1.ObjectReference, bundlePath string
287297
//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 . Unpacker
288298

289299
type Unpacker interface {
290-
UnpackBundle(lookup *operatorsv1alpha1.BundleLookup, timeout time.Duration) (result *BundleUnpackResult, err error)
300+
UnpackBundle(lookup *operatorsv1alpha1.BundleLookup, timeout, retryInterval time.Duration) (result *BundleUnpackResult, err error)
291301
}
292302

293303
type ConfigMapUnpacker struct {
@@ -448,7 +458,7 @@ const (
448458
NotUnpackedMessage = "bundle contents have not yet been persisted to installplan status"
449459
)
450460

451-
func (c *ConfigMapUnpacker) UnpackBundle(lookup *operatorsv1alpha1.BundleLookup, timeout time.Duration) (result *BundleUnpackResult, err error) {
461+
func (c *ConfigMapUnpacker) UnpackBundle(lookup *operatorsv1alpha1.BundleLookup, timeout, retryInterval time.Duration) (result *BundleUnpackResult, err error) {
452462
result = newBundleUnpackResult(lookup)
453463

454464
// if bundle lookup failed condition already present, then there is nothing more to do
@@ -510,7 +520,7 @@ func (c *ConfigMapUnpacker) UnpackBundle(lookup *operatorsv1alpha1.BundleLookup,
510520
secrets = append(secrets, corev1.LocalObjectReference{Name: secretName})
511521
}
512522
var job *batchv1.Job
513-
job, err = c.ensureJob(cmRef, result.Path, secrets, timeout)
523+
job, err = c.ensureJob(cmRef, result.Path, secrets, timeout, retryInterval)
514524
if err != nil || job == nil {
515525
// ensureJob can return nil if the job present does not match the expected job (spec and ownerefs)
516526
// The current job is deleted in that case so UnpackBundle needs to be retried
@@ -649,16 +659,39 @@ func (c *ConfigMapUnpacker) ensureConfigmap(csRef *corev1.ObjectReference, name
649659
return
650660
}
651661

652-
func (c *ConfigMapUnpacker) ensureJob(cmRef *corev1.ObjectReference, bundlePath string, secrets []corev1.LocalObjectReference, timeout time.Duration) (job *batchv1.Job, err error) {
662+
func (c *ConfigMapUnpacker) ensureJob(cmRef *corev1.ObjectReference, bundlePath string, secrets []corev1.LocalObjectReference, timeout time.Duration, unpackRetryInterval time.Duration) (job *batchv1.Job, err error) {
653663
fresh := c.job(cmRef, bundlePath, secrets, timeout)
654-
job, err = c.jobLister.Jobs(fresh.GetNamespace()).Get(fresh.GetName())
664+
var jobs, toDelete []*batchv1.Job
665+
jobs, err = c.jobLister.Jobs(fresh.GetNamespace()).List(k8slabels.ValidatedSetSelector{bundleUnpackRefLabel: cmRef.Name})
655666
if err != nil {
656-
if apierrors.IsNotFound(err) {
657-
job, err = c.client.BatchV1().Jobs(fresh.GetNamespace()).Create(context.TODO(), fresh, metav1.CreateOptions{})
658-
}
659-
660667
return
661668
}
669+
if len(jobs) == 0 {
670+
job, err = c.client.BatchV1().Jobs(fresh.GetNamespace()).Create(context.TODO(), fresh, metav1.CreateOptions{})
671+
return
672+
}
673+
674+
maxRetainedJobs := 5 // TODO: make this configurable
675+
job, toDelete = sortUnpackJobs(jobs, maxRetainedJobs) // choose latest or on-failed job attempt
676+
677+
// only check for retries if an unpackRetryInterval is specified
678+
if unpackRetryInterval > 0 {
679+
if _, isFailed := getCondition(job, batchv1.JobFailed); isFailed {
680+
// Look for other unpack jobs for the same bundle
681+
if cond, failed := getCondition(job, batchv1.JobFailed); failed {
682+
if time.Now().After(cond.LastTransitionTime.Time.Add(unpackRetryInterval)) {
683+
fresh.SetName(names.SimpleNameGenerator.GenerateName(fresh.GetName()))
684+
job, err = c.client.BatchV1().Jobs(fresh.GetNamespace()).Create(context.TODO(), fresh, metav1.CreateOptions{})
685+
}
686+
}
687+
688+
// cleanup old failed jobs, but don't clean up successful jobs to avoid repeat unpacking
689+
for _, j := range toDelete {
690+
_ = c.client.BatchV1().Jobs(j.GetNamespace()).Delete(context.TODO(), j.GetName(), metav1.DeleteOptions{})
691+
}
692+
return
693+
}
694+
}
662695

663696
if equality.Semantic.DeepDerivative(fresh.GetOwnerReferences(), job.GetOwnerReferences()) && equality.Semantic.DeepDerivative(fresh.Spec, job.Spec) {
664697
return
@@ -801,6 +834,37 @@ func getCondition(job *batchv1.Job, conditionType batchv1.JobConditionType) (con
801834
return
802835
}
803836

837+
func sortUnpackJobs(jobs []*batchv1.Job, maxRetainedJobs int) (latest *batchv1.Job, toDelete []*batchv1.Job) {
838+
if len(jobs) == 0 {
839+
return
840+
}
841+
// sort jobs so that latest job is first
842+
// with preference for non-failed jobs
843+
sort.Slice(jobs, func(i, j int) bool {
844+
condI, failedI := getCondition(jobs[i], batchv1.JobFailed)
845+
condJ, failedJ := getCondition(jobs[j], batchv1.JobFailed)
846+
if failedI != failedJ {
847+
return !failedI // non-failed job goes first
848+
}
849+
return condI.LastTransitionTime.After(condJ.LastTransitionTime.Time)
850+
})
851+
latest = jobs[0]
852+
if len(jobs) <= maxRetainedJobs {
853+
return
854+
}
855+
if maxRetainedJobs == 0 {
856+
toDelete = jobs[1:]
857+
return
858+
}
859+
860+
// cleanup old failed jobs, n-1 recent jobs and the oldest job
861+
for i := 0; i < maxRetainedJobs && i+maxRetainedJobs < len(jobs); i++ {
862+
toDelete = append(toDelete, jobs[maxRetainedJobs+i])
863+
}
864+
865+
return
866+
}
867+
804868
// OperatorGroupBundleUnpackTimeout returns bundle timeout from annotation if specified.
805869
// If the timeout annotation is not set, return timeout < 0 which is subsequently ignored.
806870
// This is to overrides the --bundle-unpack-timeout flag value on per-OperatorGroup basis.
@@ -827,3 +891,28 @@ func OperatorGroupBundleUnpackTimeout(ogLister v1listers.OperatorGroupNamespaceL
827891

828892
return d, nil
829893
}
894+
895+
// OperatorGroupBundleUnpackRetryInterval returns bundle unpack retry interval from annotation if specified.
896+
// If the retry annotation is not set, return retry = 0 which is subsequently ignored. This interval, if > 0,
897+
// determines the minimum interval between recreating a failed unpack job.
898+
func OperatorGroupBundleUnpackRetryInterval(ogLister v1listers.OperatorGroupNamespaceLister) (time.Duration, error) {
899+
ogs, err := ogLister.List(k8slabels.Everything())
900+
if err != nil {
901+
return 0, err
902+
}
903+
if len(ogs) != 1 {
904+
return 0, fmt.Errorf("found %d operatorGroups, expected 1", len(ogs))
905+
}
906+
907+
timeoutStr, ok := ogs[0].GetAnnotations()[BundleUnpackRetryMinimumIntervalAnnotationKey]
908+
if !ok {
909+
return 0, nil
910+
}
911+
912+
d, err := time.ParseDuration(timeoutStr)
913+
if err != nil {
914+
return 0, fmt.Errorf("failed to parse unpack retry annotation(%s: %s): %w", BundleUnpackRetryMinimumIntervalAnnotationKey, timeoutStr, err)
915+
}
916+
917+
return d, nil
918+
}

0 commit comments

Comments
 (0)