diff --git a/cmd/olm/cleanup_test.go b/cmd/olm/cleanup_test.go index 860535c5b3..40386bb4c7 100644 --- a/cmd/olm/cleanup_test.go +++ b/cmd/olm/cleanup_test.go @@ -478,7 +478,7 @@ func TestCleanupOwnerReferences(t *testing.T) { require.Equal(t, tt.expected.err, cleanupOwnerReferences(c, crc)) listOpts := metav1.ListOptions{} - csvs, err := crc.OperatorsV1alpha1().ClusterServiceVersions(metav1.NamespaceAll).List(listOpts) + csvs, err := crc.OperatorsV1alpha1().ClusterServiceVersions(metav1.NamespaceAll).List(context.TODO(), listOpts) require.NoError(t, err) require.ElementsMatch(t, tt.expected.csvs, csvs.Items) diff --git a/pkg/controller/operators/catalog/operator.go b/pkg/controller/operators/catalog/operator.go index 0ef7a6ca5f..7e002e269a 100644 --- a/pkg/controller/operators/catalog/operator.go +++ b/pkg/controller/operators/catalog/operator.go @@ -17,6 +17,7 @@ import ( rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" extinf "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -1305,30 +1306,6 @@ func (o *Operator) ResolvePlan(plan *v1alpha1.InstallPlan) error { return nil } -func GetCRDV1VersionsMap(crd *apiextensionsv1.CustomResourceDefinition) map[string]struct{} { - versionsMap := map[string]struct{}{} - - for _, version := range crd.Spec.Versions { - versionsMap[version.Name] = struct{}{} - } - return versionsMap -} - -// Ensure all existing served versions are present in new CRD -func EnsureCRDVersions(oldCRD *apiextensionsv1.CustomResourceDefinition, newCRD *apiextensionsv1.CustomResourceDefinition) error { - newCRDVersions := GetCRDV1VersionsMap(newCRD) - - for _, oldVersion := range oldCRD.Spec.Versions { - if oldVersion.Served { - _, ok := newCRDVersions[oldVersion.Name] - if !ok { - return fmt.Errorf("New CRD (%s) must contain existing served versions (%s)", oldCRD.Name, oldVersion.Name) - } - } - } - return nil -} - // Validate all existing served versions against new CRD's validation (if changed) func validateV1CRDCompatibility(dynamicClient dynamic.Interface, oldCRD *apiextensionsv1.CustomResourceDefinition, newCRD *apiextensionsv1.CustomResourceDefinition) error { logrus.Debugf("Comparing %#v to %#v", oldCRD.Spec.Versions, newCRD.Spec.Versions) @@ -1364,6 +1341,36 @@ func validateV1CRDCompatibility(dynamicClient dynamic.Interface, oldCRD *apiexte return nil } +// Validate all existing served versions against new CRD's validation (if changed) +func validateV1Beta1CRDCompatibility(dynamicClient dynamic.Interface, oldCRD *apiextensionsv1beta1.CustomResourceDefinition, newCRD *apiextensionsv1beta1.CustomResourceDefinition) error { + logrus.Debugf("Comparing %#v to %#v", oldCRD.Spec.Validation, newCRD.Spec.Validation) + + // TODO return early of all versions are equal + convertedCRD := &apiextensions.CustomResourceDefinition{} + if err := apiextensionsv1beta1.Convert_v1beta1_CustomResourceDefinition_To_apiextensions_CustomResourceDefinition(newCRD, convertedCRD, nil); err != nil { + return err + } + for _, version := range oldCRD.Spec.Versions { + if !version.Served { + gvr := schema.GroupVersionResource{Group: oldCRD.Spec.Group, Version: version.Name, Resource: oldCRD.Spec.Names.Plural} + err := validateExistingCRs(dynamicClient, gvr, convertedCRD) + if err != nil { + return err + } + } + } + + if oldCRD.Spec.Version != "" { + gvr := schema.GroupVersionResource{Group: oldCRD.Spec.Group, Version: oldCRD.Spec.Version, Resource: oldCRD.Spec.Names.Plural} + err := validateExistingCRs(dynamicClient, gvr, convertedCRD) + if err != nil { + return err + } + } + logrus.Debugf("Successfully validated CRD %s\n", newCRD.Name) + return nil +} + func validateExistingCRs(dynamicClient dynamic.Interface, gvr schema.GroupVersionResource, newCRD *apiextensions.CustomResourceDefinition) error { // make dynamic client crList, err := dynamicClient.Resource(gvr).List(context.TODO(), metav1.ListOptions{}) @@ -1383,33 +1390,6 @@ func validateExistingCRs(dynamicClient dynamic.Interface, gvr schema.GroupVersio return nil } -// Attempt to remove stored versions that have been deprecated before allowing -// those versions to be removed from the new CRD. -// The function may not always succeed as storedVersions requires at least one -// version. If there is only stored version, it won't be removed until a new -// stored version is added. -func removeDeprecatedV1StoredVersions(oldCRD *apiextensionsv1.CustomResourceDefinition, newCRD *apiextensionsv1.CustomResourceDefinition) []string { - // StoredVersions requires to have at least one version. - if len(oldCRD.Status.StoredVersions) <= 1 { - return nil - } - - newStoredVersions := []string{} - newCRDVersions := GetCRDV1VersionsMap(newCRD) - for _, v := range oldCRD.Status.StoredVersions { - _, ok := newCRDVersions[v] - if ok { - newStoredVersions = append(newStoredVersions, v) - } - } - - if len(newStoredVersions) < 1 { - return nil - } else { - return newStoredVersions - } -} - // ExecutePlan applies a planned InstallPlan to a namespace. func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error { if plan.Status.Phase != v1alpha1.InstallPlanPhaseInstalling { @@ -1436,7 +1416,7 @@ func (o *Operator) ExecutePlan(plan *v1alpha1.InstallPlan) error { } ensurer := newStepEnsurer(kubeclient, crclient, dynamicClient) - b := newBuilder(kubeclient, dynamicClient, o.csvProvidedAPIsIndexer) + b := newBuilder(kubeclient, dynamicClient, o.csvProvidedAPIsIndexer, o.logger) for i, step := range plan.Status.Plan { doStep := true diff --git a/pkg/controller/operators/catalog/operator_test.go b/pkg/controller/operators/catalog/operator_test.go index e13bf023cb..4ecebcbe8d 100644 --- a/pkg/controller/operators/catalog/operator_test.go +++ b/pkg/controller/operators/catalog/operator_test.go @@ -135,203 +135,6 @@ func TestTransitionInstallPlan(t *testing.T) { } } -func TestEnsureV1CRDVersions(t *testing.T) { - mainCRDPlural := "ins-main-abcde" - - currentVersions := []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Storage: true, - }, - } - - addedVersions := []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Storage: false, - }, - { - Name: "v1alpha2", - Served: true, - Storage: true, - }, - } - - missingVersions := []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha2", - Served: true, - Storage: true, - }, - } - - tests := []struct { - name string - oldCRD apiextensionsv1.CustomResourceDefinition - newCRD apiextensionsv1.CustomResourceDefinition - expectedFailure bool - }{ - { - name: "existing versions are present", - oldCRD: func() apiextensionsv1.CustomResourceDefinition { - oldCRD := v1crd(mainCRDPlural) - oldCRD.Spec.Versions = currentVersions - return oldCRD - }(), - newCRD: func() apiextensionsv1.CustomResourceDefinition { - newCRD := v1crd(mainCRDPlural) - newCRD.Spec.Versions = addedVersions - return newCRD - }(), - expectedFailure: false, - }, - { - name: "missing versions in new CRD 1", - oldCRD: func() apiextensionsv1.CustomResourceDefinition { - oldCRD := v1crd(mainCRDPlural) - oldCRD.Spec.Versions = currentVersions - return oldCRD - }(), - newCRD: func() apiextensionsv1.CustomResourceDefinition { - newCRD := v1crd(mainCRDPlural) - newCRD.Spec.Versions = missingVersions - return newCRD - }(), - expectedFailure: true, - }, - { - name: "missing version in new CRD 2", - oldCRD: func() apiextensionsv1.CustomResourceDefinition { - oldCRD := v1crd(mainCRDPlural) - oldCRD.Spec.Versions[0].Name = "v1alpha1" - return oldCRD - }(), - newCRD: func() apiextensionsv1.CustomResourceDefinition { - newCRD := v1crd(mainCRDPlural) - newCRD.Spec.Versions[0].Name = "v1alpha2" - return newCRD - }(), - expectedFailure: true, - }, - { - name: "existing version is present in new CRD's versions", - oldCRD: func() apiextensionsv1.CustomResourceDefinition { - oldCRD := v1crd(mainCRDPlural) - oldCRD.Spec.Versions[0].Name = "v1alpha1" - return oldCRD - }(), - newCRD: func() apiextensionsv1.CustomResourceDefinition { - newCRD := v1crd(mainCRDPlural) - newCRD.Spec.Versions = addedVersions - return newCRD - }(), - expectedFailure: false, - }, - } - - for _, tt := range tests { - err := EnsureCRDVersions(&tt.oldCRD, &tt.newCRD) - if tt.expectedFailure { - require.Error(t, err) - } - } -} - -func TestRemoveDeprecatedV1StoredVersions(t *testing.T) { - mainCRDPlural := "ins-main-test" - - currentVersions := []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Storage: false, - }, - { - Name: "v1alpha2", - Served: true, - Storage: true, - }, - } - - newVersions := []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1alpha2", - Served: true, - Storage: false, - }, - { - Name: "apiextensionsv1beta1", - Served: true, - Storage: true, - }, - } - - crdStatusStoredVersions := apiextensionsv1.CustomResourceDefinitionStatus{ - StoredVersions: []string{}, - } - - tests := []struct { - name string - oldCRD apiextensionsv1.CustomResourceDefinition - newCRD apiextensionsv1.CustomResourceDefinition - expectedResult []string - }{ - { - name: "only one stored version exists", - oldCRD: func() apiextensionsv1.CustomResourceDefinition { - oldCRD := v1crd(mainCRDPlural) - oldCRD.Spec.Versions = currentVersions - oldCRD.Status = crdStatusStoredVersions - oldCRD.Status.StoredVersions = []string{"v1alpha1"} - return oldCRD - }(), - newCRD: func() apiextensionsv1.CustomResourceDefinition { - newCRD := v1crd(mainCRDPlural) - newCRD.Spec.Versions = newVersions - return newCRD - }(), - expectedResult: nil, - }, - { - name: "multiple stored versions with one deprecated version", - oldCRD: func() apiextensionsv1.CustomResourceDefinition { - oldCRD := v1crd(mainCRDPlural) - oldCRD.Spec.Versions = currentVersions - oldCRD.Status.StoredVersions = []string{"v1alpha1", "v1alpha2"} - return oldCRD - }(), - newCRD: func() apiextensionsv1.CustomResourceDefinition { - newCRD := v1crd(mainCRDPlural) - newCRD.Spec.Versions = newVersions - return newCRD - }(), - expectedResult: []string{"v1alpha2"}, - }, - { - name: "multiple stored versions with all deprecated version", - oldCRD: func() apiextensionsv1.CustomResourceDefinition { - oldCRD := v1crd(mainCRDPlural) - oldCRD.Spec.Versions = currentVersions - oldCRD.Status.StoredVersions = []string{"v1alpha1", "v1alpha3"} - return oldCRD - }(), - newCRD: func() apiextensionsv1.CustomResourceDefinition { - newCRD := v1crd(mainCRDPlural) - newCRD.Spec.Versions = newVersions - return newCRD - }(), - expectedResult: nil, - }, - } - - for _, tt := range tests { - resultCRD := removeDeprecatedV1StoredVersions(&tt.oldCRD, &tt.newCRD) - require.Equal(t, tt.expectedResult, resultCRD) - } -} - func TestExecutePlan(t *testing.T) { namespace := "ns" diff --git a/pkg/controller/operators/catalog/step.go b/pkg/controller/operators/catalog/step.go index 326338f19f..8cb7ec659f 100644 --- a/pkg/controller/operators/catalog/step.go +++ b/pkg/controller/operators/catalog/step.go @@ -13,7 +13,10 @@ import ( errorwrap "github.com/pkg/errors" logger "github.com/sirupsen/logrus" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + apiextensionsv1beta1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -36,13 +39,16 @@ type builder struct { opclient operatorclient.ClientInterface dynamicClient dynamic.Interface csvToProvidedAPIs map[string]cache.Indexer + logger logger.FieldLogger } -func newBuilder(opclient operatorclient.ClientInterface, dynamicClient dynamic.Interface, csvToProvidedAPIs map[string]cache.Indexer) *builder { +func newBuilder(opclient operatorclient.ClientInterface, dynamicClient dynamic.Interface, csvToProvidedAPIs map[string]cache.Indexer, + logger logger.FieldLogger) *builder { return &builder{ opclient: opclient, dynamicClient: dynamicClient, csvToProvidedAPIs: csvToProvidedAPIs, + logger: logger, } } @@ -54,26 +60,33 @@ func (n notSupportedStepperErr) Error() string { return n.message } -// step is a factory that creates StepperFuncs based on the Kind provided and the install plan step. +// step is a factory that creates StepperFuncs based on the install plan step Kind. func (b *builder) create(step *v1alpha1.Step) (Stepper, error) { - kind := step.Resource.Kind - switch kind { + switch step.Resource.Kind { case crdKind: - return b.NewCRDStep(step.Resource.Manifest, b.opclient.ApiextensionsInterface(), step.Status, step.Resource.Name), nil - default: - return nil, notSupportedStepperErr{fmt.Sprintf("stepper interface does not support %s", kind)} + version, err := crdlib.Version(&step.Resource.Manifest) + if err != nil { + return nil, err + } + switch version { + case crdlib.V1Version: + return b.NewCRDV1Step(b.opclient.ApiextensionsInterface().ApiextensionsV1(), step), nil + case crdlib.V1Beta1Version: + return b.NewCRDV1Beta1Step(b.opclient.ApiextensionsInterface().ApiextensionsV1beta1(), step), nil + } } + return nil, notSupportedStepperErr{fmt.Sprintf("stepper interface does not support %s", step.Resource.Kind)} } -func (b *builder) NewCRDStep(manifest string, client clientset.Interface, status v1alpha1.StepStatus, name string) StepperFunc { +func (b *builder) NewCRDV1Step(client apiextensionsv1client.ApiextensionsV1Interface, step *v1alpha1.Step) StepperFunc { return func() (v1alpha1.StepStatus, error) { - switch status { + switch step.Status { case v1alpha1.StepStatusPresent: return v1alpha1.StepStatusPresent, nil case v1alpha1.StepStatusCreated: return v1alpha1.StepStatusCreated, nil case v1alpha1.StepStatusWaitingForAPI: - crd, err := client.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), name, metav1.GetOptions{}) + crd, err := client.CustomResourceDefinitions().Get(context.TODO(), step.Resource.Name, metav1.GetOptions{}) if err != nil { if k8serrors.IsNotFound(err) { return v1alpha1.StepStatusNotPresent, nil @@ -98,60 +111,127 @@ func (b *builder) NewCRDStep(manifest string, client clientset.Interface, status return v1alpha1.StepStatusCreated, nil } case v1alpha1.StepStatusUnknown, v1alpha1.StepStatusNotPresent: - crd, err := crdlib.Serialize(manifest) + crd, err := crdlib.UnmarshalV1(step.Resource.Manifest) if err != nil { return v1alpha1.StepStatusUnknown, err } + b.logger.Debugf("creating v1 CRD %#v", crd) - _, err = client.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}) - if k8serrors.IsAlreadyExists(err) { - currentCRD, _ := client.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), crd.GetName(), metav1.GetOptions{}) - // Compare 2 CRDs to see if it needs to be updatetd - if crdlib.NotEqual(currentCRD, crd) { - // Verify CRD ownership, only attempt to update if - // CRD has only one owner - // Example: provided=database.coreos.com/v1alpha1/EtcdCluster - matchedCSV, err := index.CRDProviderNames(b.csvToProvidedAPIs, crd) - if err != nil { - return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error find matched CSV: %s", name) + _, createError := client.CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}) + if k8serrors.IsAlreadyExists(createError) { + currentCRD, _ := client.CustomResourceDefinitions().Get(context.TODO(), crd.GetName(), metav1.GetOptions{}) + b.logger.Debugf("\n current crd: %#v \n new crd: %#v \n", currentCRD, crd) + // Verify CRD ownership, only attempt to update if + // CRD has only one owner + // Example: provided=database.coreos.com/v1alpha1/EtcdCluster + matchedCSV, err := index.V1CRDProviderNames(b.csvToProvidedAPIs, crd) + if err != nil { + return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error find matched CSV: %s", step.Resource.Name) + } + crd.SetResourceVersion(currentCRD.GetResourceVersion()) + if len(matchedCSV) == 1 { + logger.Debugf("Found one owner for CRD %v", crd) + } else if len(matchedCSV) > 1 { + logger.Debugf("Found multiple owners for CRD %v", crd) + + if err = validateV1CRDCompatibility(b.dynamicClient, currentCRD, crd); err != nil { + return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error validating existing CRs agains new CRD's schema: %s", step.Resource.Name) } - crd.SetResourceVersion(currentCRD.GetResourceVersion()) - if len(matchedCSV) == 1 { - logger.Debugf("Found one owner for CRD %v", crd) - } else if len(matchedCSV) > 1 { - logger.Debugf("Found multiple owners for CRD %v", crd) - - err := EnsureCRDVersions(currentCRD, crd) - if err != nil { - return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error missing existing CRD version(s) in new CRD: %s", name) - } - - if err = validateV1CRDCompatibility(b.dynamicClient, currentCRD, crd); err != nil { - return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error validating existing CRs agains new CRD's schema: %s", name) - } + } + + // TODO ensure stored version compatibility + // Update CRD to new version + _, err = client.CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}) + if err != nil { + return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error updating CRD: %s", step.Resource.Name) + } + // If it already existed, mark the step as Present. + // they were equal - mark CRD as present + return v1alpha1.StepStatusPresent, nil + } else if createError != nil { + // Unexpected error creating the CRD. + return v1alpha1.StepStatusUnknown, createError + } + // If no error occured, make sure to wait for the API to become available. + return v1alpha1.StepStatusWaitingForAPI, nil + } + return v1alpha1.StepStatusUnknown, nil + } +} + +func (b *builder) NewCRDV1Beta1Step(client apiextensionsv1beta1client.ApiextensionsV1beta1Interface, step *v1alpha1.Step) StepperFunc { + return func() (v1alpha1.StepStatus, error) { + switch step.Status { + case v1alpha1.StepStatusPresent: + return v1alpha1.StepStatusPresent, nil + case v1alpha1.StepStatusCreated: + return v1alpha1.StepStatusCreated, nil + case v1alpha1.StepStatusWaitingForAPI: + crd, err := client.CustomResourceDefinitions().Get(context.TODO(), step.Resource.Name, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + return v1alpha1.StepStatusNotPresent, nil + } else { + return v1alpha1.StepStatusNotPresent, errorwrap.Wrapf(err, "error finding the %s CRD", crd.Name) + } + } + established, namesAccepted := false, false + for _, cdt := range crd.Status.Conditions { + switch cdt.Type { + case apiextensionsv1beta1.Established: + if cdt.Status == apiextensionsv1beta1.ConditionTrue { + established = true } - // Remove deprecated version in CRD storedVersions - storeVersions := removeDeprecatedV1StoredVersions(currentCRD, crd) - if storeVersions != nil { - currentCRD.Status.StoredVersions = storeVersions - resultCRD, err := client.ApiextensionsV1().CustomResourceDefinitions().UpdateStatus(context.TODO(), currentCRD, metav1.UpdateOptions{}) - if err != nil { - return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error updating CRD's status: %s", name) - } - crd.SetResourceVersion(resultCRD.GetResourceVersion()) + case apiextensionsv1beta1.NamesAccepted: + if cdt.Status == apiextensionsv1beta1.ConditionTrue { + namesAccepted = true } - // Update CRD to new version - _, err = client.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}) - if err != nil { - return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error updating CRD: %s", name) + } + } + if established && namesAccepted { + return v1alpha1.StepStatusCreated, nil + } + case v1alpha1.StepStatusUnknown, v1alpha1.StepStatusNotPresent: + crd, err := crdlib.UnmarshalV1Beta1(step.Resource.Manifest) + if err != nil { + return v1alpha1.StepStatusUnknown, err + } + b.logger.Debugf("creating v1beta1 CRD %#v", crd) + + _, createError := client.CustomResourceDefinitions().Create(context.TODO(), crd, metav1.CreateOptions{}) + if k8serrors.IsAlreadyExists(createError) { + currentCRD, _ := client.CustomResourceDefinitions().Get(context.TODO(), crd.GetName(), metav1.GetOptions{}) + b.logger.Debugf("\n current crd: %#v \n new crd: %#v \n", currentCRD, crd) + // Verify CRD ownership, only attempt to update if + // CRD has only one owner + // Example: provided=database.coreos.com/v1alpha1/EtcdCluster + matchedCSV, err := index.V1Beta1CRDProviderNames(b.csvToProvidedAPIs, crd) + if err != nil { + return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error find matched CSV: %s", step.Resource.Name) + } + crd.SetResourceVersion(currentCRD.GetResourceVersion()) + if len(matchedCSV) == 1 { + logger.Debugf("Found one owner for CRD %v", crd) + } else if len(matchedCSV) > 1 { + logger.Debugf("Found multiple owners for CRD %v", crd) + + if err = validateV1Beta1CRDCompatibility(b.dynamicClient, currentCRD, crd); err != nil { + return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error validating existing CRs agains new CRD's schema: %s", step.Resource.Name) } } + // TODO ensure stored version compatibility + + // Update CRD to new version + _, err = client.CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{}) + if err != nil { + return v1alpha1.StepStatusUnknown, errorwrap.Wrapf(err, "error updating CRD: %s", step.Resource.Name) + } // If it already existed, mark the step as Present. // they were equal - mark CRD as present return v1alpha1.StepStatusPresent, nil - } else if err != nil { + } else if createError != nil { // Unexpected error creating the CRD. - return v1alpha1.StepStatusUnknown, err + return v1alpha1.StepStatusUnknown, createError } // If no error occured, make sure to wait for the API to become available. return v1alpha1.StepStatusWaitingForAPI, nil diff --git a/pkg/lib/crd/serialize.go b/pkg/lib/crd/serialize.go deleted file mode 100644 index 913beca369..0000000000 --- a/pkg/lib/crd/serialize.go +++ /dev/null @@ -1,68 +0,0 @@ -package crd - -import ( - "bytes" - "fmt" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" - - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/yaml" -) - -const ( - Kind = "CustomResourceDefinition" - APIVersion = "apiextensions.k8s.io/v1" -) - -var ( - scheme = runtime.NewScheme() -) - -func init() { - // Add conversions between CRD versions - install.Install(scheme) -} - -// Serialize takes in a CRD manifest and returns a v1 versioned CRD object. -// Compatible with v1beta1 or v1 CRD manifests. -func Serialize(manifest string) (*apiextensionsv1.CustomResourceDefinition, error) { - u := &unstructured.Unstructured{} - reader := bytes.NewReader([]byte(manifest)) - decoder := yaml.NewYAMLOrJSONDecoder(reader, 30) - if err := decoder.Decode(u); err != nil { - return nil, fmt.Errorf("crd unmarshaling failed: %s", err) - } - - - // Step through unversioned type to support v1beta1 -> v1 - unversioned := &apiextensions.CustomResourceDefinition{} - if err := scheme.Convert(u, unversioned, nil); err != nil { - return nil, fmt.Errorf("failed to convert crd from unstructured to internal: %s\nto v1: %s", u, err) - } - - crd := &apiextensionsv1.CustomResourceDefinition{} - if err := scheme.Convert(unversioned, crd, nil); err != nil { - return nil, fmt.Errorf("failed to convert crd from internal to v1: %s\nto v1: %s", u, err) - } - - // set CRD type meta - // for purposes of fake client for unit tests to pass - crd.TypeMeta.Kind = Kind - crd.TypeMeta.APIVersion = APIVersion - - - // for each version in the CRD, check and make sure there is a schema - // if not a schema, give a default schema of props - for i := range crd.Spec.Versions { - if crd.Spec.Versions[i].Schema == nil { - schema := &apiextensionsv1.JSONSchemaProps{Type: "object"} - crd.Spec.Versions[i].Schema = &apiextensionsv1.CustomResourceValidation{OpenAPIV3Schema:schema} - } - } - - - return crd, nil -} diff --git a/pkg/lib/crd/unmarshal.go b/pkg/lib/crd/unmarshal.go new file mode 100644 index 0000000000..d4b143fa23 --- /dev/null +++ b/pkg/lib/crd/unmarshal.go @@ -0,0 +1,50 @@ +package crd + +import ( + "bytes" + "fmt" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/yaml" +) + +const ( + Kind = "CustomResourceDefinition" + Group = "apiextensions.k8s.io/" +) + +var ( + scheme = runtime.NewScheme() +) + +func init() { + // Add conversions between CRD versions + install.Install(scheme) +} + +// UnmarshalV1 takes in a CRD manifest and returns a v1 versioned CRD object. +func UnmarshalV1(manifest string) (*apiextensionsv1.CustomResourceDefinition, error) { + crd := &apiextensionsv1.CustomResourceDefinition{} + reader := bytes.NewReader([]byte(manifest)) + decoder := yaml.NewYAMLOrJSONDecoder(reader, 30) + if err := decoder.Decode(crd); err != nil { + return nil, fmt.Errorf("v1 crd unmarshaling failed: %s", err) + } + + return crd, nil +} + +// UnmarshalV1 takes in a CRD manifest and returns a v1beta1 versioned CRD object. +func UnmarshalV1Beta1(manifest string) (*apiextensionsv1beta1.CustomResourceDefinition, error) { + crd := &apiextensionsv1beta1.CustomResourceDefinition{} + reader := bytes.NewReader([]byte(manifest)) + decoder := yaml.NewYAMLOrJSONDecoder(reader, 30) + if err := decoder.Decode(crd); err != nil { + return nil, fmt.Errorf("v1beta1 crd unmarshaling failed: %s", err) + } + + return crd, nil +} diff --git a/pkg/lib/crd/version.go b/pkg/lib/crd/version.go index 66f02e048e..efa78313c2 100644 --- a/pkg/lib/crd/version.go +++ b/pkg/lib/crd/version.go @@ -2,10 +2,8 @@ package crd import ( "fmt" - "reflect" "strings" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/yaml" ) @@ -23,42 +21,21 @@ var supportedCRDVersions = map[string]struct{}{ // Version takes a CRD manifest and determines whether it is v1 or v1beta1 type based on the APIVersion. func Version(manifest *string) (string, error) { + if manifest == nil { + return "", fmt.Errorf("empty CRD manifest") + } + dec := yaml.NewYAMLOrJSONDecoder(strings.NewReader(*manifest), 10) unst := &unstructured.Unstructured{} if err := dec.Decode(unst); err != nil { return "", err } - v := unst.GetObjectKind().GroupVersionKind().Version + v := unst.GroupVersionKind().Version if _, ok := supportedCRDVersions[v]; !ok { - return "", fmt.Errorf("could not determine CRD version from manifest") + return "", fmt.Errorf("CRD APIVersion from manifest not supported: %s", v) } return v, nil } -// NotEqual determines whether two CRDs are equal based on the versions and validations of both. -// NotEqual looks at the range of the old CRD versions to ensure index out of bounds errors do not occur. -// If true, then we know we need to update the CRD on cluster. -func NotEqual(currentCRD *apiextensionsv1.CustomResourceDefinition, oldCRD *apiextensionsv1.CustomResourceDefinition) bool { - var equalVersions bool - var equalValidation bool - var oldRange = len(oldCRD.Spec.Versions) - 1 - - equalVersions = reflect.DeepEqual(currentCRD.Spec.Versions, oldCRD.Spec.Versions) - if !equalVersions { - return true - } - - for i := range currentCRD.Spec.Versions { - if i > oldRange { - return true - } - equalValidation = reflect.DeepEqual(currentCRD.Spec.Versions[i].Schema, oldCRD.Spec.Versions[i].Schema) - if equalValidation == false { - return true - } - } - - return false -} diff --git a/pkg/lib/index/api.go b/pkg/lib/index/api.go index 686d5b399e..3a719bcb75 100644 --- a/pkg/lib/index/api.go +++ b/pkg/lib/index/api.go @@ -6,6 +6,8 @@ import ( "github.com/operator-framework/api/pkg/operators/v1alpha1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/client-go/tools/cache" ) @@ -38,7 +40,7 @@ func ProvidedAPIsIndexFunc(obj interface{}) ([]string, error) { } // CRDProviderNames returns the names of CSVs that own the given CRD -func CRDProviderNames(indexers map[string]cache.Indexer, crd *apiextensionsv1.CustomResourceDefinition) (map[string]struct{}, error) { +func V1CRDProviderNames(indexers map[string]cache.Indexer, crd *apiextensionsv1.CustomResourceDefinition) (map[string]struct{}, error) { csvSet := map[string]struct{}{} crdSpec := map[string]struct{}{} for _, v := range crd.Spec.Versions { @@ -63,3 +65,32 @@ func CRDProviderNames(indexers map[string]cache.Indexer, crd *apiextensionsv1.Cu } return csvSet, nil } + +// CRDProviderNames returns the names of CSVs that own the given CRD +func V1Beta1CRDProviderNames(indexers map[string]cache.Indexer, crd *apiextensionsv1beta1.CustomResourceDefinition) (map[string]struct{}, error) { + csvSet := map[string]struct{}{} + crdSpec := map[string]struct{}{} + for _, v := range crd.Spec.Versions { + crdSpec[fmt.Sprintf("%s/%s/%s", crd.Spec.Group, v.Name, crd.Spec.Names.Kind)] = struct{}{} + } + if crd.Spec.Version != "" { + crdSpec[fmt.Sprintf("%s/%s/%s", crd.Spec.Group, crd.Spec.Version, crd.Spec.Names.Kind)] = struct{}{} + } + for _, indexer := range indexers { + for key := range crdSpec { + csvs, err := indexer.ByIndex(ProvidedAPIsIndexFuncKey, key) + if err != nil { + return nil, err + } + for _, item := range csvs { + csv, ok := item.(*v1alpha1.ClusterServiceVersion) + if !ok { + continue + } + // Add to set + csvSet[csv.GetName()] = struct{}{} + } + } + } + return csvSet, nil +} diff --git a/test/e2e/crd_e2e_test.go b/test/e2e/crd_e2e_test.go new file mode 100644 index 0000000000..a3ed2e6de1 --- /dev/null +++ b/test/e2e/crd_e2e_test.go @@ -0,0 +1,187 @@ +package e2e + +import ( + "fmt" + "github.com/blang/semver" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + crdName = "test" + crdNamePlural = "tests" + crdGroup = "test.k8s.io" +) + +var _ = Describe("CRD Versions", func() { + It("creates v1beta1 crds with a v1beta1 schema successfully", func() { + By("This test proves that OLM is able to handle v1beta1 CRDs successfully. Creating v1 CRDs has more " + + "restrictions around the schema. v1beta1 validation schemas are not necessarily valid in v1. " + + "OLM should support both v1beta1 and v1 CRDs") + c := newKubeClient(GinkgoT()) + crc := newCRClient(GinkgoT()) + + mainPackageName := genName("nginx-update2-") + mainPackageStable := fmt.Sprintf("%s-stable", mainPackageName) + stableChannel := "stable" + mainNamedStrategy := newNginxInstallStrategy(genName("dep-"), nil, nil) + + crdPlural := genName("ins-v1beta1-") + crdName := crdPlural + ".cluster.com" + mainCRD := apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: crdName, + }, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "cluster.com", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Served: true, + Storage: true, + }, + }, + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: crdPlural, + Singular: crdPlural, + Kind: crdPlural, + ListKind: "list" + crdPlural, + }, + Scope: "Namespaced", + // this validation is not a valid v1 structural schema because the "type: object" field is missing + Validation: &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Description: "my crd schema", + }, + }, + }, + } + + mainCSV := newCSV(mainPackageStable, testNamespace, "", semver.MustParse("0.1.0"), nil, nil, mainNamedStrategy) + mainCatalogName := genName("mock-ocs-main-update2-") + mainManifests := []registry.PackageManifest{ + { + PackageName: mainPackageName, + Channels: []registry.PackageChannel{ + {Name: stableChannel, CurrentCSVName: mainPackageStable}, + }, + DefaultChannelName: stableChannel, + }, + } + + // Create the catalog sources + _, cleanupMainCatalogSource := createInternalCatalogSource(GinkgoT(), c, crc, mainCatalogName, testNamespace, mainManifests, []apiextensions.CustomResourceDefinition{mainCRD}, []operatorsv1alpha1.ClusterServiceVersion{mainCSV}) + defer cleanupMainCatalogSource() + + // Attempt to get the catalog source before creating install plan + _, err := fetchCatalogSource(GinkgoT(), crc, mainCatalogName, testNamespace, catalogSourceRegistryPodSynced) + Expect(err).ToNot(HaveOccurred()) + + subscriptionName := genName("sub-nginx-update2-") + subscriptionCleanup := createSubscriptionForCatalog(GinkgoT(), crc, testNamespace, subscriptionName, mainCatalogName, mainPackageName, stableChannel, "", operatorsv1alpha1.ApprovalAutomatic) + defer subscriptionCleanup() + + subscription, err := fetchSubscription(GinkgoT(), crc, testNamespace, subscriptionName, subscriptionHasInstallPlanChecker) + Expect(err).ToNot(HaveOccurred()) + Expect(subscription).ToNot(Equal(nil)) + Expect(subscription.Status.InstallPlanRef).ToNot(Equal(nil)) + Expect(mainCSV.GetName()).To(Equal(subscription.Status.CurrentCSV)) + + installPlanName := subscription.Status.InstallPlanRef.Name + + // Wait for InstallPlan to be status: Complete before checking resource presence + fetchedInstallPlan, err := fetchInstallPlan(GinkgoT(), crc, installPlanName, buildInstallPlanPhaseCheckFunc(operatorsv1alpha1.InstallPlanPhaseComplete)) + Expect(err).ToNot(HaveOccurred()) + GinkgoT().Logf("Install plan %s fetched with status %s", fetchedInstallPlan.GetName(), fetchedInstallPlan.Status.Phase) + Expect(fetchedInstallPlan.Status.Phase).To(Equal(operatorsv1alpha1.InstallPlanPhaseComplete)) + }) + It("creates v1 CRDs with a v1 schema successfully", func() { + By("v1 crds with a valid openapiv3 schema should be created successfully by OLM") + c := newKubeClient(GinkgoT()) + crc := newCRClient(GinkgoT()) + + mainPackageName := genName("nginx-update2-") + mainPackageStable := fmt.Sprintf("%s-stable", mainPackageName) + stableChannel := "stable" + mainNamedStrategy := newNginxInstallStrategy(genName("dep-"), nil, nil) + + crdPlural := genName("ins-v1beta1-") + crdName := crdPlural + ".cluster.com" + v1crd := apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: crdName, + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "cluster.com", + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1alpha1", + Served: true, + Storage: true, + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Description: "my crd schema", + }, + }, + }, + }, + }, + } + + mainCSV := newCSV(mainPackageStable, testNamespace, "", semver.MustParse("0.1.0"), nil, nil, mainNamedStrategy) + mainCatalogName := genName("mock-ocs-main-update2-") + mainManifests := []registry.PackageManifest{ + { + PackageName: mainPackageName, + Channels: []registry.PackageChannel{ + {Name: stableChannel, CurrentCSVName: mainPackageStable}, + }, + DefaultChannelName: stableChannel, + }, + } + + // Create the catalog sources + _, cleanupMainCatalogSource := createV1CRDInternalCatalogSource(GinkgoT(), c, crc, mainCatalogName, testNamespace, mainManifests, []apiextensionsv1.CustomResourceDefinition{v1crd}, []operatorsv1alpha1.ClusterServiceVersion{mainCSV}) + defer cleanupMainCatalogSource() + + // Attempt to get the catalog source before creating install plan + _, err := fetchCatalogSource(GinkgoT(), crc, mainCatalogName, testNamespace, catalogSourceRegistryPodSynced) + Expect(err).ToNot(HaveOccurred()) + + subscriptionName := genName("sub-nginx-update2-") + subscriptionCleanup := createSubscriptionForCatalog(GinkgoT(), crc, testNamespace, subscriptionName, mainCatalogName, mainPackageName, stableChannel, "", operatorsv1alpha1.ApprovalAutomatic) + defer subscriptionCleanup() + + subscription, err := fetchSubscription(GinkgoT(), crc, testNamespace, subscriptionName, subscriptionHasInstallPlanChecker) + Expect(err).ToNot(HaveOccurred()) + Expect(subscription).ToNot(Equal(nil)) + Expect(subscription.Status.InstallPlanRef).ToNot(Equal(nil)) + Expect(mainCSV.GetName()).To(Equal(subscription.Status.CurrentCSV)) + + installPlanName := subscription.Status.InstallPlanRef.Name + + // Wait for InstallPlan to be status: Complete before checking resource presence + fetchedInstallPlan, err := fetchInstallPlan(GinkgoT(), crc, installPlanName, buildInstallPlanPhaseCheckFunc(operatorsv1alpha1.InstallPlanPhaseComplete)) + Expect(err).ToNot(HaveOccurred()) + GinkgoT().Logf("Install plan %s fetched with status %s", fetchedInstallPlan.GetName(), fetchedInstallPlan.Status.Phase) + Expect(fetchedInstallPlan.Status.Phase).To(Equal(operatorsv1alpha1.InstallPlanPhaseComplete)) + }) + AfterEach(func() { cleaner.NotifyTestComplete(GinkgoT(), true) }, float64(10)) +}) + +func checkCRD(v1crd *apiextensionsv1.CustomResourceDefinition) bool { + for _, condition := range v1crd.Status.Conditions { + if condition.Type == apiextensionsv1.Established { + return true + } + } + + return false +} diff --git a/test/e2e/crd_upgrade_e2e_test.go b/test/e2e/crd_upgrade_e2e_test.go deleted file mode 100644 index a9904e9815..0000000000 --- a/test/e2e/crd_upgrade_e2e_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package e2e - -import ( - "context" - "time" - - "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/catalog" - "github.com/operator-framework/operator-lifecycle-manager/test/e2e/ctx" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - crdName = "test" - crdNamePlural = "tests" - crdGroup = "test.k8s.io" -) - -var _ = Describe("CRD APIVersion upgrades", func() { - It("Handles CRD versioning changes as expected", func() { - By("CRDs changed APIVersions from v1beta1 to v1 and OLM must support both versions. " + - "Upgrading from a v1beta1 to a v1 version of the same CRD should be seamless because the client always returns the latest version.") - - c := ctx.Ctx().KubeClient() - - oldv1beta1CRD := &apiextensionsv1beta1.CustomResourceDefinition{ - TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1beta1", Kind: "CustomResourceDefinition"}, - ObjectMeta: metav1.ObjectMeta{ - Name: crdNamePlural + "." + crdGroup, - }, - Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ - Group: crdGroup, - Version: "v1", - Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ - Singular: crdName, - Plural: crdNamePlural, - Kind: "test", - }, - }, - } - - // create v1beta1 CRD on server - oldcrd, err := c.ApiextensionsInterface().ApiextensionsV1beta1().CustomResourceDefinitions().Create(context.TODO(), oldv1beta1CRD, metav1.CreateOptions{}) - Expect(err).ToNot(HaveOccurred()) - By("created CRD") - - // poll for CRD to be ready (using the v1 client) - Eventually(func() (bool, error) { - fetchedCRD, err := c.ApiextensionsInterface().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), oldcrd.GetName(), metav1.GetOptions{}) - if err != nil || fetchedCRD == nil { - return false, err - } - return checkCRD(fetchedCRD), nil - }, 5*time.Minute, 10*time.Second).Should(Equal(true)) - - oldCRDConvertedToV1, err := c.ApiextensionsInterface().ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), oldcrd.GetName(), metav1.GetOptions{}) - Expect(err).ToNot(HaveOccurred()) - - // confirm the v1 crd as is as expected - // run ensureCRDV1Versions on results - newCRD := &apiextensionsv1.CustomResourceDefinition{ - ObjectMeta: metav1.ObjectMeta{ - Name: crdNamePlural + "." + crdGroup, - }, - Spec: apiextensionsv1.CustomResourceDefinitionSpec{ - Group: crdGroup, - Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ - { - Name: "v1", - Served: true, - }, - }, - Names: apiextensionsv1.CustomResourceDefinitionNames{ - Singular: crdName, - Plural: crdNamePlural, - }, - }, - } - - err = catalog.EnsureCRDVersions(oldCRDConvertedToV1, newCRD) - Expect(err).ToNot(HaveOccurred()) - }) - AfterEach(func() { cleaner.NotifyTestComplete(GinkgoT(), true) }, float64(10)) -}) - -func checkCRD(v1crd *apiextensionsv1.CustomResourceDefinition) bool { - for _, condition := range v1crd.Status.Conditions { - if condition.Type == apiextensionsv1.Established { - return true - } - } - - return false -} diff --git a/test/e2e/installplan_e2e_test.go b/test/e2e/installplan_e2e_test.go index cc655160e7..31eeef89a1 100644 --- a/test/e2e/installplan_e2e_test.go +++ b/test/e2e/installplan_e2e_test.go @@ -487,9 +487,9 @@ var _ = Describe("Install Plan", func() { newCRD *apiextensions.CustomResourceDefinition } - var min float64 = 2.1 - var max float64 = 256.1 - var newMax float64 = 50.1 + var min float64 = 2 + var max float64 = 256 + var newMax float64 = 50 // generated outside of the test table so that the same naming can be used for both old and new CSVs mainCRDPlural := "testcrd" @@ -527,7 +527,6 @@ var _ = Describe("Install Plan", func() { }, }, } - return &oldCRD }(), newCRD: func() *apiextensions.CustomResourceDefinition { @@ -537,46 +536,27 @@ var _ = Describe("Install Plan", func() { { Name: "v1alpha1", Served: true, - Storage: true, - Schema: &apiextensions.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensions.JSONSchemaProps{ - "spec": { - Type: "object", - Description: "Spec of a test object.", - Properties: map[string]apiextensions.JSONSchemaProps{ - "scalar": { - Type: "number", - Description: "Scalar value that should have a min and max.", - Minimum: &min, - Maximum: &max, - }, - }, - }, - }, - }, - }, + Storage: false, }, { Name: "v1alpha2", Served: true, - Storage: false, - Schema: &apiextensions.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ - Type: "object", + Storage: true, + }, + } + newCRD.Spec.Validation = &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensions.JSONSchemaProps{ + "spec": { + Type: "object", + Description: "Spec of a test object.", Properties: map[string]apiextensions.JSONSchemaProps{ - "spec": { - Type: "object", - Description: "Spec of a test object.", - Properties: map[string]apiextensions.JSONSchemaProps{ - "scalar": { - Type: "number", - Description: "Scalar value that should have a min and max.", - Minimum: &min, - Maximum: &max, - }, - }, + "scalar": { + Type: "number", + Description: "Scalar value that should have a min and max.", + Minimum: &min, + Maximum: &max, }, }, }, @@ -607,22 +587,22 @@ var _ = Describe("Install Plan", func() { { Name: "v1alpha1", Served: true, - Storage: false, - Schema: &apiextensions.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ - Type: "object", + Storage: true, + }, + } + newCRD.Spec.Validation = &apiextensions.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensions.JSONSchemaProps{ + "spec": { + Type: "object", + Description: "Spec of a test object.", Properties: map[string]apiextensions.JSONSchemaProps{ - "spec": { - Type: "object", - Description: "Spec of a test object.", - Properties: map[string]apiextensions.JSONSchemaProps{ - "scalar": { - Type: "number", - Description: "Scalar value that should have a min and max.", - Minimum: &min, - Maximum: &newMax, - }, - }, + "scalar": { + Type: "number", + Description: "Scalar value that should have a min and max.", + Minimum: &min, + Maximum: &newMax, }, }, }, @@ -633,7 +613,7 @@ var _ = Describe("Install Plan", func() { }(), }), table.Entry("missing existing versions in new CRD", schemaPayload{name: "missing existing versions in new CRD", - expectedPhase: operatorsv1alpha1.InstallPlanPhaseFailed, + expectedPhase: operatorsv1alpha1.InstallPlanPhaseComplete, oldCRD: func() *apiextensions.CustomResourceDefinition { oldCRD := newCRD(mainCRDPlural + "c") oldCRD.Spec.Version = "" @@ -939,6 +919,11 @@ var _ = Describe("Install Plan", func() { Served: true, Storage: false, }, + { + Name: "v1alpha1", + Served: false, + Storage: false, + }, } return &newCRD }(), @@ -1116,6 +1101,7 @@ var _ = Describe("Install Plan", func() { expectedVersions = map[string]struct{}{ "v1alpha2": {}, "v1beta1": {}, + "v1alpha1": {}, } validateCRDVersions(GinkgoT(), c, tt.oldCRD.GetName(), expectedVersions) @@ -2315,8 +2301,8 @@ var _ = Describe("Install Plan", func() { crdPlural := genName("ins") crdName := crdPlural + ".cluster.com" - var min float64 = 2.1 - var max float64 = 256.1 + var min float64 = 2 + var max float64 = 256 // Create CRD with offending property crd := apiextensions.CustomResourceDefinition{ @@ -2326,32 +2312,6 @@ var _ = Describe("Install Plan", func() { Spec: apiextensions.CustomResourceDefinitionSpec{ Group: "cluster.com", Version: "v1alpha1", - Versions: []apiextensions.CustomResourceDefinitionVersion{ - { - Name: "v1alpha1", - Served: true, - Storage: true, - Schema: &apiextensions.CustomResourceValidation{ - OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ - Type: "object", - Properties: map[string]apiextensions.JSONSchemaProps{ - "spec": { - Type: "object", - Description: "Spec of a test object.", - Properties: map[string]apiextensions.JSONSchemaProps{ - "scalar": { - Type: "number", - Description: "Scalar value that should have a min and max.", - Minimum: &min, - Maximum: &max, - }, - }, - }, - }, - }, - }, - }, - }, Validation: &apiextensions.CustomResourceValidation{ OpenAPIV3Schema: &apiextensions.JSONSchemaProps{ Type: "object", diff --git a/test/e2e/util_test.go b/test/e2e/util_test.go index d227f186ef..20dc5b15c0 100644 --- a/test/e2e/util_test.go +++ b/test/e2e/util_test.go @@ -15,6 +15,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" extScheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" "k8s.io/apimachinery/pkg/api/equality" @@ -483,6 +484,40 @@ func createInternalCatalogSource(t GinkgoTInterface, c operatorclient.ClientInte return catalogSource, cleanupInternalCatalogSource } +func createV1CRDInternalCatalogSource(t GinkgoTInterface, c operatorclient.ClientInterface, crc versioned.Interface, name, namespace string, manifests []registry.PackageManifest, crds []apiextensionsv1.CustomResourceDefinition, csvs []v1alpha1.ClusterServiceVersion) (*v1alpha1.CatalogSource, cleanupFunc) { + configMap, configMapCleanup := createV1CRDConfigMapForCatalogData(t, c, name, namespace, manifests, crds, csvs) + + // Create an internal CatalogSource custom resource pointing to the ConfigMap + catalogSource := &v1alpha1.CatalogSource{ + TypeMeta: metav1.TypeMeta{ + Kind: v1alpha1.CatalogSourceKind, + APIVersion: v1alpha1.CatalogSourceCRDAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha1.CatalogSourceSpec{ + SourceType: "internal", + ConfigMap: configMap.GetName(), + }, + } + catalogSource.SetNamespace(namespace) + + t.Logf("Creating catalog source %s in namespace %s...", name, namespace) + catalogSource, err := crc.OperatorsV1alpha1().CatalogSources(namespace).Create(context.TODO(), catalogSource, metav1.CreateOptions{}) + if err != nil && !apierrors.IsAlreadyExists(err) { + require.NoError(t, err) + } + t.Logf("Catalog source %s created", name) + + cleanupInternalCatalogSource := func() { + configMapCleanup() + buildCatalogSourceCleanupFunc(t, crc, namespace, catalogSource)() + } + return catalogSource, cleanupInternalCatalogSource +} + func createConfigMapForCatalogData(t GinkgoTInterface, c operatorclient.ClientInterface, name, namespace string, manifests []registry.PackageManifest, crds []apiextensions.CustomResourceDefinition, csvs []v1alpha1.ClusterServiceVersion) (*corev1.ConfigMap, cleanupFunc) { // Create a config map containing the PackageManifests and CSVs configMapName := fmt.Sprintf("%s-configmap", name) @@ -529,6 +564,52 @@ func createConfigMapForCatalogData(t GinkgoTInterface, c operatorclient.ClientIn return createdConfigMap, buildConfigMapCleanupFunc(t, c, namespace, createdConfigMap) } +func createV1CRDConfigMapForCatalogData(t GinkgoTInterface, c operatorclient.ClientInterface, name, namespace string, manifests []registry.PackageManifest, crds []apiextensionsv1.CustomResourceDefinition, csvs []v1alpha1.ClusterServiceVersion) (*corev1.ConfigMap, cleanupFunc) { + // Create a config map containing the PackageManifests and CSVs + configMapName := fmt.Sprintf("%s-configmap", name) + catalogConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + Data: map[string]string{}, + } + catalogConfigMap.SetNamespace(namespace) + + // Add raw manifests + if manifests != nil { + manifestsRaw, err := yaml.Marshal(manifests) + require.NoError(t, err) + catalogConfigMap.Data[registry.ConfigMapPackageName] = string(manifestsRaw) + } + + // Add raw CRDs + var crdsRaw []byte + if crds != nil { + crdStrings := []string{} + for _, crd := range crds { + crdStrings = append(crdStrings, serializeV1CRD(t, &crd)) + } + var err error + crdsRaw, err = yaml.Marshal(crdStrings) + require.NoError(t, err) + } + catalogConfigMap.Data[registry.ConfigMapCRDName] = strings.Replace(string(crdsRaw), "- |\n ", "- ", -1) + + // Add raw CSVs + if csvs != nil { + csvsRaw, err := yaml.Marshal(csvs) + require.NoError(t, err) + catalogConfigMap.Data[registry.ConfigMapCSVName] = string(csvsRaw) + } + + createdConfigMap, err := c.KubernetesInterface().CoreV1().ConfigMaps(namespace).Create(context.TODO(), catalogConfigMap, metav1.CreateOptions{}) + if err != nil && !apierrors.IsAlreadyExists(err) { + require.NoError(t, err) + } + return createdConfigMap, buildConfigMapCleanupFunc(t, c, namespace, createdConfigMap) +} + func serializeCRD(t GinkgoTInterface, crd apiextensions.CustomResourceDefinition) string { scheme := runtime.NewScheme() require.NoError(t, extScheme.AddToScheme(scheme)) @@ -551,6 +632,19 @@ func serializeCRD(t GinkgoTInterface, crd apiextensions.CustomResourceDefinition return manifest.String() } +func serializeV1CRD(t GinkgoTInterface, crd *apiextensionsv1.CustomResourceDefinition) string { + scheme := runtime.NewScheme() + require.NoError(t, apiextensionsv1.AddToScheme(scheme)) + + // set up object serializer + serializer := k8sjson.NewYAMLSerializer(k8sjson.DefaultMetaFactory, scheme, scheme) + + // create an object manifest + var manifest bytes.Buffer + require.NoError(t, serializer.Encode(crd, &manifest)) + return manifest.String() +} + func createCR(c operatorclient.ClientInterface, item *unstructured.Unstructured, apiGroup, version, namespace, resourceKind, resourceName string) (cleanupFunc, error) { err := c.CreateCustomResource(item) if err != nil {