Skip to content

Commit

Permalink
rename Catalog to ClusterCatalog
Browse files Browse the repository at this point in the history
Signed-off-by: everettraven <everettraven@gmail.com>
  • Loading branch information
everettraven committed May 14, 2024
1 parent ca57eb0 commit acda61b
Show file tree
Hide file tree
Showing 5 changed files with 1,590 additions and 0 deletions.
149 changes: 149 additions & 0 deletions api/core/v1alpha1/clustercatalog_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// TODO: The source types, reason, etc. are all copy/pasted from the rukpak
// repository. We should look into whether it is possible to share these.

type SourceType string

const (
SourceTypeImage SourceType = "image"

TypeUnpacked = "Unpacked"
TypeDelete = "Delete"

ReasonUnpackPending = "UnpackPending"
ReasonUnpacking = "Unpacking"
ReasonUnpackSuccessful = "UnpackSuccessful"
ReasonUnpackFailed = "UnpackFailed"
ReasonStorageFailed = "FailedToStore"
ReasonStorageDeleteFailed = "FailedToDelete"

PhasePending = "Pending"
PhaseUnpacking = "Unpacking"
PhaseFailing = "Failing"
PhaseUnpacked = "Unpacked"
)

//+kubebuilder:object:root=true
//+kubebuilder:resource:scope=Cluster
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
//+kubebuilder:printcolumn:name=Age,type=date,JSONPath=`.metadata.creationTimestamp`

// ClusterCatalog is the Schema for the Catalogs API
type ClusterCatalog struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec ClusterCatalogSpec `json:"spec,omitempty"`
Status ClusterCatalogStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// ClusterCatalogList contains a list of Catalog
type ClusterCatalogList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []ClusterCatalog `json:"items"`
}

// ClusterCatalogSpec defines the desired state of Catalog
// +kubebuilder:validation:XValidation:rule="!has(self.source.image.pollInterval) || (self.source.image.ref.find('@sha256:') == \"\")",message="cannot specify PollInterval while using digest-based image"
type ClusterCatalogSpec 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
Source CatalogSource `json:"source"`
}

// ClusterCatalogStatus defines the observed state of Catalog
type ClusterCatalogStatus struct {
// Conditions store the status conditions of the Catalog instances
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`

// ResolvedSource contains information about the resolved source
ResolvedSource *ResolvedCatalogSource `json:"resolvedSource,omitempty"`
// Phase represents a human-readable status of resolution of the content source.
// It is not appropriate to use for business logic determination.
Phase string `json:"phase,omitempty"`
// ContentURL is a cluster-internal address that on-cluster components
// can read the content of a catalog from
ContentURL string `json:"contentURL,omitempty"`
// observedGeneration is the most recent generation observed for this Catalog. It corresponds to the
// Catalog's generation, which is updated on mutation by the API Server.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}

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

// ResolvedCatalogSource contains the information about a sourced Catalog
type ResolvedCatalogSource struct {
// Type defines the kind of Catalog content that was sourced.
Type SourceType `json:"type"`
// Image is the catalog image that backs the content of this catalog.
Image *ResolvedImageSource `json:"image,omitempty"`
}

// ResolvedImageSource contains information about the sourced Catalog
type ResolvedImageSource struct {
// Ref contains the reference to a container image containing Catalog contents.
Ref string `json:"ref"`
// ResolvedRef contains the resolved sha256 image ref containing Catalog contents.
ResolvedRef string `json:"resolvedRef"`
// LastPollAtempt is the time when the source resolved was last polled for new content.
LastPollAttempt metav1.Time `json:"lastPollAttempt"`
// pullSecret exists to retain compatibility with the existing v1alpha1 APIs. It will be removed in v1alpha2.
PullSecret string `json:"pullSecret,omitempty"`
}

// ImageSource contains information required for sourcing a Catalog from an OCI image
type ImageSource struct {
// Ref contains the reference to a container image containing Catalog contents.
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.
// +kubebuilder:validation:Format:=duration
PollInterval *metav1.Duration `json:"pollInterval,omitempty"`
// InsecureSkipTLSVerify indicates that TLS certificate validation should be skipped.
// If this option is specified, the HTTPS protocol will still be used to
// fetch the specified image reference.
// This should not be used in a production environment.
// +optional
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
}

func init() {
SchemeBuilder.Register(&ClusterCatalog{}, &ClusterCatalogList{})
}
142 changes: 142 additions & 0 deletions api/core/v1alpha1/clustercatalog_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_clustercatalogs.yaml")
pth := "openAPIV3Schema.properties.spec"
validator, found := validators["v1alpha1"][pth]
assert.True(t, found)

for name, tc := range map[string]struct {
spec ClusterCatalogSpec
wantErrs []string
}{
"digest based image ref, poll interval not allowed, poll interval specified": {
spec: ClusterCatalogSpec{
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: ClusterCatalogSpec{
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) //nolint:gosec
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
}
Loading

0 comments on commit acda61b

Please sign in to comment.