Skip to content

Commit

Permalink
(feature) Implement polling image source in intervals
Browse files Browse the repository at this point in the history
Implements https://docs.google.com/document/d/1iWSrWL9pYRJ5Ua3VYErkK1Q2lAusBUeDCh66Ew4lDbQ/edit?usp=sharing

Closes #180

Signed-off-by: Anik Bhattacharjee <anbhatta@redhat.com>
  • Loading branch information
anik120 committed Oct 17, 2023
1 parent bd8cfbd commit 9884d89
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 13 deletions.
9 changes: 8 additions & 1 deletion api/core/v1alpha1/catalog_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type CatalogList struct {
}

// CatalogSpec defines the desired state of Catalog
// +kubebuilder:validation:XValidation:rule="!has(self.source.image.pollInterval) || (self.source.image.pollInterval == \"\") || (self.source.image.ref.find('@sha256:') == \"\")",message="cannot specify PollInterval while using digest-based image"
type CatalogSpec struct {
// Source is the source of a Catalog that contains Operators' metadata in the FBC format
// https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs
Expand All @@ -86,14 +87,16 @@ type CatalogStatus struct {
// ContentURL is a cluster-internal address that on-cluster components
// can read the content of a catalog from
ContentURL string `json:"contentURL,omitempty"`
// LastPollAttempt is the time when the source resolved was last polled for new content.
LastPollAttempt metav1.Time `json:"lastPollAttempt,omitempty"`
}

// CatalogSource contains the sourcing information for a Catalog
type CatalogSource struct {
// Type defines the kind of Catalog content being sourced.
Type SourceType `json:"type"`
// Image is the catalog image that backs the content of this catalog.
Image *ImageSource `json:"image,omitempty"`
Image *ImageSource `json:"image"`
}

// ImageSource contains information required for sourcing a Catalog from an OCI image
Expand All @@ -102,6 +105,10 @@ type ImageSource struct {
Ref string `json:"ref"`
// PullSecret contains the name of the image pull secret in the namespace that catalogd is deployed.
PullSecret string `json:"pullSecret,omitempty"`
// PollInterval indicates the interval at which the image source should be polled for new content,
// specified as a duration (e.g., "5m", "1h", "24h", "etc".). Note that PollInterval may not be
// specified for a catalog image referenced by a sha256 digest.
PollInterval *metav1.Duration `json:"pollInterval,omitempty"`
}

func init() {
Expand Down
142 changes: 142 additions & 0 deletions api/core/v1alpha1/catalog_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package v1alpha1

import (
"context"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"sigs.k8s.io/yaml"
)

func TestPollIntervalCELValidationRules(t *testing.T) {
validators := fieldValidatorsFromFile(t, "../../../config/crd/bases/catalogd.operatorframework.io_catalogs.yaml")
pth := "openAPIV3Schema.properties.spec"
validator, found := validators["v1alpha1"][pth]
assert.True(t, found)

for name, tc := range map[string]struct {
spec CatalogSpec
wantErrs []string
}{
"digest based image ref, poll interval not allowed, poll interval specified": {
spec: CatalogSpec{
Source: CatalogSource{
Type: SourceTypeImage,
Image: &ImageSource{
Ref: "docker.io/test-image@sha256:asdf98234sd",
PollInterval: &metav1.Duration{Duration: time.Minute},
},
},
},
wantErrs: []string{
"openAPIV3Schema.properties.spec: Invalid value: \"object\": cannot specify PollInterval while using digest-based image",
},
},
"digest based image ref, poll interval not allowed, poll interval not specified": {
spec: CatalogSpec{
Source: CatalogSource{
Type: SourceTypeImage,
Image: &ImageSource{
Ref: "docker.io/example/test-catalog@sha256:asdf123",
},
},
},
wantErrs: []string{},
},
} {
t.Run(name, func(t *testing.T) {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&tc.spec)
require.NoError(t, err)
errs := validator(obj, nil)
require.Equal(t, len(tc.wantErrs), len(errs))
for i := range tc.wantErrs {
got := errs[i].Error()
assert.Equal(t, tc.wantErrs[i], got)
}
})
}
}

// fieldValidatorsFromFile extracts the CEL validators by version and JSONPath from a CRD file and returns
// a validator func for testing against samples.
func fieldValidatorsFromFile(t *testing.T, crdFilePath string) map[string]map[string]CELValidateFunc {
data, err := os.ReadFile(crdFilePath)
require.NoError(t, err)

var crd apiextensionsv1.CustomResourceDefinition
err = yaml.Unmarshal(data, &crd)
require.NoError(t, err)

ret := map[string]map[string]CELValidateFunc{}
for _, v := range crd.Spec.Versions {
var internalSchema apiextensions.JSONSchemaProps
err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(v.Schema.OpenAPIV3Schema, &internalSchema, nil)
require.NoError(t, err, "failed to convert JSONSchemaProps for version %s: %v", v.Name, err)
structuralSchema, err := schema.NewStructural(&internalSchema)
require.NoError(t, err, "failed to create StructuralSchema for version %s: %v", v.Name, err)

versionVals, err := findCEL(structuralSchema, true, field.NewPath("openAPIV3Schema"))
require.NoError(t, err, "failed to find CEL for version %s: %v", v.Name, err)
ret[v.Name] = versionVals
}
return ret
}

// CELValidateFunc tests a sample object against a CEL validator.
type CELValidateFunc func(obj, old interface{}) field.ErrorList

func findCEL(s *schema.Structural, root bool, pth *field.Path) (map[string]CELValidateFunc, error) {
ret := map[string]CELValidateFunc{}

if len(s.XValidations) > 0 {
s := *s
pth := *pth
ret[pth.String()] = func(obj, old interface{}) field.ErrorList {
errs, _ := cel.NewValidator(&s, root, celconfig.PerCallLimit).Validate(context.TODO(), &pth, &s, obj, old, celconfig.RuntimeCELCostBudget)
return errs
}
}

for k, v := range s.Properties {
v := v
sub, err := findCEL(&v, false, pth.Child("properties").Child(k))
if err != nil {
return nil, err
}

for pth, val := range sub {
ret[pth] = val
}
}
if s.Items != nil {
sub, err := findCEL(s.Items, false, pth.Child("items"))
if err != nil {
return nil, err
}
for pth, val := range sub {
ret[pth] = val
}
}
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
sub, err := findCEL(s.AdditionalProperties.Structural, false, pth.Child("additionalProperties"))
if err != nil {
return nil, err
}
for pth, val := range sub {
ret[pth] = val
}
}

return ret, nil
}
8 changes: 7 additions & 1 deletion api/core/v1alpha1/zz_generated.deepcopy.go

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

25 changes: 25 additions & 0 deletions config/crd/bases/catalogd.operatorframework.io_catalogs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ spec:
description: Image is the catalog image that backs the content
of this catalog.
properties:
pollInterval:
description: PollInterval indicates the interval at which
the image source should be polled for new content, specified
as a duration (e.g., "5m", "1h", "24h", "etc".). Note that
PollInterval may not be specified for a catalog image referenced
by a sha256 digest.
type: string
pullSecret:
description: PullSecret contains the name of the image pull
secret in the namespace that catalogd is deployed.
Expand All @@ -64,11 +71,16 @@ spec:
description: Type defines the kind of Catalog content being sourced.
type: string
required:
- image
- type
type: object
required:
- source
type: object
x-kubernetes-validations:
- message: cannot specify PollInterval while using digest-based image
rule: '!has(self.source.image.pollInterval) || (self.source.image.pollInterval
== "") || (self.source.image.ref.find(''@sha256:'') == "")'
status:
description: CatalogStatus defines the observed state of Catalog
properties:
Expand Down Expand Up @@ -146,6 +158,11 @@ spec:
description: ContentURL is a cluster-internal address that on-cluster
components can read the content of a catalog from
type: string
lastPollAttempt:
description: LastPollAttempt is the time when the source resolved
was last polled for new content.
format: date-time
type: string
phase:
type: string
resolvedSource:
Expand All @@ -156,6 +173,13 @@ spec:
description: Image is the catalog image that backs the content
of this catalog.
properties:
pollInterval:
description: PollInterval indicates the interval at which
the image source should be polled for new content, specified
as a duration (e.g., "5m", "1h", "24h", "etc".). Note that
PollInterval may not be specified for a catalog image referenced
by a sha256 digest.
type: string
pullSecret:
description: PullSecret contains the name of the image pull
secret in the namespace that catalogd is deployed.
Expand All @@ -171,6 +195,7 @@ spec:
description: Type defines the kind of Catalog content being sourced.
type: string
required:
- image
- type
type: object
type: object
Expand Down
1 change: 1 addition & 0 deletions config/samples/core_v1alpha1_catalog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ spec:
source:
type: image
image:
pollInterval: 24h
ref: quay.io/operatorhubio/catalog:latest
14 changes: 11 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.3
k8s.io/api v0.27.2
k8s.io/apiextensions-apiserver v0.27.2
k8s.io/apimachinery v0.27.2
k8s.io/apiserver v0.27.2
k8s.io/client-go v0.27.2
k8s.io/component-base v0.27.2
sigs.k8s.io/controller-runtime v0.15.0
sigs.k8s.io/yaml v1.3.0
)

require (
cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
Expand All @@ -38,6 +41,8 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.9.8 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/aws/aws-sdk-go-v2 v1.17.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.15 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect
Expand Down Expand Up @@ -81,6 +86,7 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/cel-go v0.15.3 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
Expand All @@ -97,6 +103,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand All @@ -109,12 +116,14 @@ require (
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/sirupsen/logrus v1.9.2 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
Expand All @@ -126,16 +135,15 @@ require (
golang.org/x/tools v0.9.1 // indirect
gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230525154841-bd750badd5c6 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.27.2 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230308161112-d77c459e9343 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
Loading

0 comments on commit 9884d89

Please sign in to comment.