-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: everettraven <everettraven@gmail.com>
- Loading branch information
1 parent
ca57eb0
commit acda61b
Showing
5 changed files
with
1,590 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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{}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.