From a458282a4ca0e30191a3554432cccb15fa08060c Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Tue, 14 May 2024 13:13:20 -0400 Subject: [PATCH] REMOVE extension api from main branch (#820) --- Makefile | 5 +- api/v1alpha1/clusterextension_types.go | 5 + api/v1alpha1/extension_types.go | 159 ---- api/v1alpha1/zz_generated.deepcopy.go | 142 ---- cmd/manager/main.go | 8 - config/rbac/role.yaml | 36 - go.mod | 2 - go.sum | 6 - .../clusterextension_controller.go | 17 + internal/controllers/common_controller.go | 33 - .../controllers/extension_admission_test.go | 298 ------- internal/controllers/extension_controller.go | 570 -------------- .../controllers/extension_controller_test.go | 240 ------ internal/controllers/suite_test.go | 10 - pkg/scheme/scheme.go | 2 - scripts/install.tpl.sh | 9 +- testdata/crds/kappctrl.k14s.io_app.yaml | 728 ------------------ 17 files changed, 25 insertions(+), 2245 deletions(-) delete mode 100644 api/v1alpha1/extension_types.go delete mode 100644 internal/controllers/extension_admission_test.go delete mode 100644 internal/controllers/extension_controller.go delete mode 100644 internal/controllers/extension_controller_test.go delete mode 100644 testdata/crds/kappctrl.k14s.io_app.yaml diff --git a/Makefile b/Makefile index 6557870bc..10f656b6c 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,6 @@ IMG := $(IMAGE_REPO):$(IMAGE_TAG) # Define dependency versions (use go.mod if we also use Go code from dependency) export CERT_MGR_VERSION := v1.9.0 export CATALOGD_VERSION := $(shell go list -mod=mod -m -f "{{.Version}}" github.com/operator-framework/catalogd) -export KAPP_VERSION := $(shell go list -mod=mod -m -f "{{.Version}}" github.com/vmware-tanzu/carvel-kapp-controller) export RUKPAK_VERSION := $(shell go list -mod=mod -m -f "{{.Version}}" github.com/operator-framework/rukpak) export WAIT_TIMEOUT := 60s @@ -166,7 +165,7 @@ kind-load: $(KIND) #EXHELP Loads the currently constructed image onto the cluste kind-deploy: export MANIFEST := ./operator-controller.yaml kind-deploy: manifests $(KUSTOMIZE) #EXHELP Install controller and dependencies onto the kind cluster. $(KUSTOMIZE) build $(KUSTOMIZE_BUILD_DIR) > operator-controller.yaml - envsubst '$$CATALOGD_VERSION,$$CERT_MGR_VERSION,$$KAPP_VERSION,$$RUKPAK_VERSION,$$MANIFEST' < scripts/install.tpl.sh | bash -s + envsubst '$$CATALOGD_VERSION,$$CERT_MGR_VERSION,$$RUKPAK_VERSION,$$MANIFEST' < scripts/install.tpl.sh | bash -s .PHONY: kind-cluster kind-cluster: $(KIND) #EXHELP Standup a kind cluster. @@ -256,7 +255,7 @@ release: $(GORELEASER) #EXHELP Runs goreleaser for the operator-controller. By d quickstart: export MANIFEST := https://github.com/operator-framework/operator-controller/releases/download/$(VERSION)/operator-controller.yaml quickstart: $(KUSTOMIZE) manifests #EXHELP Generate the installation release manifests and scripts. $(KUSTOMIZE) build $(KUSTOMIZE_BUILD_DIR) | sed "s/:devel/:$(VERSION)/g" > operator-controller.yaml - envsubst '$$CATALOGD_VERSION,$$CERT_MGR_VERSION,$$KAPP_VERSION,$$RUKPAK_VERSION,$$MANIFEST' < scripts/install.tpl.sh > install.sh + envsubst '$$CATALOGD_VERSION,$$CERT_MGR_VERSION,$$RUKPAK_VERSION,$$MANIFEST' < scripts/install.tpl.sh > install.sh ##@ Docs diff --git a/api/v1alpha1/clusterextension_types.go b/api/v1alpha1/clusterextension_types.go index cd87db021..11d7e7b13 100644 --- a/api/v1alpha1/clusterextension_types.go +++ b/api/v1alpha1/clusterextension_types.go @@ -121,6 +121,11 @@ func init() { ) } +type BundleMetadata struct { + Name string `json:"name"` + Version string `json:"version"` +} + // ClusterExtensionStatus defines the observed state of ClusterExtension type ClusterExtensionStatus struct { // +optional diff --git a/api/v1alpha1/extension_types.go b/api/v1alpha1/extension_types.go deleted file mode 100644 index 6109c2cc8..000000000 --- a/api/v1alpha1/extension_types.go +++ /dev/null @@ -1,159 +0,0 @@ -/* -Copyright 2024. - -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" - - "github.com/operator-framework/operator-controller/internal/conditionsets" -) - -const ( - SourceTypePackage = "package" -) - -type ExtensionSourcePackage struct { - //+kubebuilder:validation:MaxLength:=48 - //+kubebuilder:validation:Pattern:=^[a-z0-9]+(-[a-z0-9]+)*$ - // name specifies the name of the name of the package - Name string `json:"name"` - - //+kubebuilder:validation:MaxLength:=64 - //+kubebuilder:validation:Pattern=`^(\s*(=||!=|>|<|>=|=>|<=|=<|~|~>|\^)\s*(v?(0|[1-9]\d*|[x|X|\*])(\.(0|[1-9]\d*|x|X|\*]))?(\.(0|[1-9]\d*|x|X|\*))?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)\s*)((?:\s+|,\s*|\s*\|\|\s*)(=||!=|>|<|>=|=>|<=|=<|~|~>|\^)\s*(v?(0|[1-9]\d*|x|X|\*])(\.(0|[1-9]\d*|x|X|\*))?(\.(0|[1-9]\d*|x|X|\*]))?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)\s*)*$` - //+kubebuilder:Optional - // Version is an optional semver constraint on the package version. If not specified, the latest version available of the package will be installed. - // If specified, the specific version of the package will be installed so long as it is available in any of the content sources available. - // Examples: 1.2.3, 1.0.0-alpha, 1.0.0-rc.1 - // - // For more information on semver, please see https://semver.org/ - // version constraint definition - Version string `json:"version,omitempty"` - - //+kubebuilder:validation:MaxLength:=48 - //+kubebuilder:validation:Pattern:=^[a-z0-9]+([\.-][a-z0-9]+)*$ - // channel constraint definition - Channel string `json:"channel,omitempty"` - - //+kubebuilder:validation:Enum:=Enforce;Ignore - //+kubebuilder:default:=Enforce - //+kubebuilder:Optional - // - // upgradeConstraintPolicy Defines the policy for how to handle upgrade constraints - UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"` -} - -// +kubebuilder:validation:XValidation:rule="self.sourceType=='package' && has(self.__package__)",message="sourceType must match populated union field" -// -// ExtensionSource defines the source for this Extension, right now, only a package is supported. -type ExtensionSource struct { - //+kubebuilder:validation:Enum:=package - //+kubebuilder:validation:Required - // sourceType is the discriminator for the source type - SourceType string `json:"sourceType"` - - // package defines a reference for a bundle in a catalog defined by a name and a version and/or channel - Package *ExtensionSourcePackage `json:"package,omitempty"` -} - -// ExtensionSpec defines the desired state of Extension -type ExtensionSpec struct { - //+kubebuilder:Optional - // - // paused controls the management state of the extension. If the extension is paused, it will be ignored by the extension controller. - Paused bool `json:"paused,omitempty"` - - //+kubebuilder:validation:MaxLength:=253 - //+kubebuilder:validation:Pattern:=^[a-z0-9]+([\.-][a-z0-9]+)*$ - // - // serviceAccountName is the name of a service account in the Extension's namespace that will be used to manage the installation and lifecycle of the extension. - ServiceAccountName string `json:"serviceAccountName"` - - // source of Extension to be installed - Source ExtensionSource `json:"source"` - - //+kubebuilder:Optional - // - // skipCRDUpgradeSafetyCheck specifies whether or not the CRD upgrade safety checks should be skipped when attempting to install the extension - SkipCRDUpgradeSafetyCheck bool `json:"skipCRDUpgradeSafetyCheck,omitempty"` -} - -// ExtensionStatus defines the observed state of Extension -type ExtensionStatus struct { - // paused indicates the current reconciliation state of this extension - Paused bool `json:"paused"` - - // +optional - InstalledBundle *BundleMetadata `json:"installedBundle,omitempty"` - // +optional - ResolvedBundle *BundleMetadata `json:"resolvedBundle,omitempty"` - - // +patchMergeKey=type - // +patchStrategy=merge - // +listType=map - // +listMapKey=type - Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` -} - -type BundleMetadata struct { - Name string `json:"name"` - Version string `json:"version"` -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:printcolumn:name="Paused",type=string,JSONPath=`.status.paused`,description="The current reconciliation state of this extension" - -// Extension is the Schema for the extensions API -type Extension struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ExtensionSpec `json:"spec,omitempty"` - Status ExtensionStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// ExtensionList contains a list of Extension -type ExtensionList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []Extension `json:"items"` -} - -const ( - // TypeProgressing indicates whether operator-controller is - // reconciling, installing, updating or deleting an extension. - TypeProgressing = "Progressing" - - ReasonProgressing = "Progressing" - ReasonFailedToReachDesiredIntent = "FailedToReachDesiredIntent" - ReasonReachedDesiredIntent = "ReachedDesiredIntent" -) - -func init() { - SchemeBuilder.Register(&Extension{}, &ExtensionList{}) - - conditionsets.ExtensionConditionTypes = []string{ - TypeProgressing, - } - conditionsets.ExtensionConditionReasons = []string{ - ReasonProgressing, - ReasonFailedToReachDesiredIntent, - ReasonReachedDesiredIntent, - } -} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index bc1276b2f..1d981d93c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -145,145 +145,3 @@ func (in *ClusterExtensionStatus) DeepCopy() *ClusterExtensionStatus { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Extension) DeepCopyInto(out *Extension) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extension. -func (in *Extension) DeepCopy() *Extension { - if in == nil { - return nil - } - out := new(Extension) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Extension) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExtensionList) DeepCopyInto(out *ExtensionList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Extension, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionList. -func (in *ExtensionList) DeepCopy() *ExtensionList { - if in == nil { - return nil - } - out := new(ExtensionList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ExtensionList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExtensionSource) DeepCopyInto(out *ExtensionSource) { - *out = *in - if in.Package != nil { - in, out := &in.Package, &out.Package - *out = new(ExtensionSourcePackage) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionSource. -func (in *ExtensionSource) DeepCopy() *ExtensionSource { - if in == nil { - return nil - } - out := new(ExtensionSource) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExtensionSourcePackage) DeepCopyInto(out *ExtensionSourcePackage) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionSourcePackage. -func (in *ExtensionSourcePackage) DeepCopy() *ExtensionSourcePackage { - if in == nil { - return nil - } - out := new(ExtensionSourcePackage) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExtensionSpec) DeepCopyInto(out *ExtensionSpec) { - *out = *in - in.Source.DeepCopyInto(&out.Source) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionSpec. -func (in *ExtensionSpec) DeepCopy() *ExtensionSpec { - if in == nil { - return nil - } - out := new(ExtensionSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExtensionStatus) DeepCopyInto(out *ExtensionStatus) { - *out = *in - if in.InstalledBundle != nil { - in, out := &in.InstalledBundle, &out.InstalledBundle - *out = new(BundleMetadata) - **out = **in - } - if in.ResolvedBundle != nil { - in, out := &in.ResolvedBundle, &out.ResolvedBundle - *out = new(BundleMetadata) - **out = **in - } - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionStatus. -func (in *ExtensionStatus) DeepCopy() *ExtensionStatus { - if in == nil { - return nil - } - out := new(ExtensionStatus) - in.DeepCopyInto(out) - return out -} diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 56b9e6bfe..855d80c3e 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -108,14 +108,6 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "ClusterExtension") os.Exit(1) } - - if err = (&controllers.ExtensionReconciler{ - Client: cl, - BundleProvider: catalogClient, - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Extension") - os.Exit(1) - } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 7b53723b7..4652565c6 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -29,17 +29,6 @@ rules: - patch - update - watch -- apiGroups: - - kappctrl.k14s.io - resources: - - apps - verbs: - - create - - get - - list - - patch - - update - - watch - apiGroups: - olm.operatorframework.io resources: @@ -61,28 +50,3 @@ rules: verbs: - patch - update -- apiGroups: - - olm.operatorframework.io - resources: - - extensions - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - olm.operatorframework.io - resources: - - extensions/finalizers - verbs: - - update -- apiGroups: - - olm.operatorframework.io - resources: - - extensions/status - verbs: - - patch - - update diff --git a/go.mod b/go.mod index e4442fbe7..38770c70b 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/operator-framework/rukpak v0.20.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 - github.com/vmware-tanzu/carvel-kapp-controller v0.51.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 gopkg.in/yaml.v2 v2.4.0 @@ -27,7 +26,6 @@ require ( ) require ( - carvel.dev/vendir v0.40.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index f665c6c2d..a6d81cff6 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,5 @@ -carvel.dev/vendir v0.40.0 h1:JdhCp/EjAPGI8F5zoAVYwZHf1sPEFee19RpgGb3ciT8= -carvel.dev/vendir v0.40.0/go.mod h1:XPdluJu7322RZNx05AA4gYnV52aKywBdh7Ma12GuM2Q= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -95,7 +92,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= @@ -133,8 +129,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vmware-tanzu/carvel-kapp-controller v0.51.0 h1:lCCHy9n/AzWPtq5gqbINJHgmF32RCUkh9DbVQgx6HAs= -github.com/vmware-tanzu/carvel-kapp-controller v0.51.0/go.mod h1:go1MQz1D2kVgjaE2ZHtuHGECFk8EDLeXMpjmDNDzuJM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/internal/controllers/clusterextension_controller.go b/internal/controllers/clusterextension_controller.go index 45298ec85..d0d663b25 100644 --- a/internal/controllers/clusterextension_controller.go +++ b/internal/controllers/clusterextension_controller.go @@ -23,6 +23,7 @@ import ( "strings" mmsemver "github.com/Masterminds/semver/v3" + bsemver "github.com/blang/semver/v4" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/equality" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -555,3 +556,19 @@ func clusterExtensionRequestsForCatalog(c client.Reader, logger logr.Logger) han return requests } } + +// bundleMetadataFor returns a BundleMetadata for the given bundle. If the provided bundle is nil, +// this function panics. It is up to the caller to ensure that the bundle is non-nil. +func bundleMetadataFor(bundle *catalogmetadata.Bundle) *ocv1alpha1.BundleMetadata { + if bundle == nil { + panic("programmer error: provided bundle must be non-nil to create BundleMetadata") + } + ver, err := bundle.Version() + if err != nil { + ver = &bsemver.Version{} + } + return &ocv1alpha1.BundleMetadata{ + Name: bundle.Name, + Version: ver.String(), + } +} diff --git a/internal/controllers/common_controller.go b/internal/controllers/common_controller.go index 377ad3040..8bf0b8e5f 100644 --- a/internal/controllers/common_controller.go +++ b/internal/controllers/common_controller.go @@ -106,36 +106,3 @@ func setDeprecationStatusesUnknown(conditions *[]metav1.Condition, message strin }) } } - -// setProgressingStatusConditionSuccess sets the progressing status condition to false for a successful install or upgrade. -func setProgressingStatusConditionSuccess(conditions *[]metav1.Condition, message string, generation int64) { - apimeta.SetStatusCondition(conditions, metav1.Condition{ - Type: ocv1alpha1.TypeProgressing, - Status: metav1.ConditionFalse, - Reason: ocv1alpha1.ReasonReachedDesiredIntent, - Message: message, - ObservedGeneration: generation, - }) -} - -// setProgressingStatusConditionFailed sets the progressing status condition to False for a failed install or upgrade. -func setProgressingStatusConditionFailed(conditions *[]metav1.Condition, message string, generation int64) { - apimeta.SetStatusCondition(conditions, metav1.Condition{ - Type: ocv1alpha1.TypeProgressing, - Status: metav1.ConditionFalse, - Reason: ocv1alpha1.ReasonFailedToReachDesiredIntent, - Message: message, - ObservedGeneration: generation, - }) -} - -// setProgressingStatusConditionProgressing sets the progressing status condition to true for an app being reconciled. -func setProgressingStatusConditionProgressing(conditions *[]metav1.Condition, message string, generation int64) { - apimeta.SetStatusCondition(conditions, metav1.Condition{ - Type: ocv1alpha1.TypeProgressing, - Status: metav1.ConditionTrue, - Reason: ocv1alpha1.ReasonProgressing, - Message: message, - ObservedGeneration: generation, - }) -} diff --git a/internal/controllers/extension_admission_test.go b/internal/controllers/extension_admission_test.go deleted file mode 100644 index f97bdf6c6..000000000 --- a/internal/controllers/extension_admission_test.go +++ /dev/null @@ -1,298 +0,0 @@ -package controllers_test - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/require" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" -) - -func TestExtensionAdmissionServiceAccount(t *testing.T) { - tooLongError := "spec.serviceAccountName: Too long: may not be longer than 253" - regexMismatchError := "spec.serviceAccountName in body should match" - - testCases := []struct { - name string - saName string - errMsg string - }{ - {"no service account", "", regexMismatchError}, - {"hypen-separated", "hyphenated-name", ""}, - {"dot-separated", "dotted.name", ""}, - {"long channel name", fmt.Sprintf("longname%s", strings.Repeat("0", 256)), tooLongError}, - {"spaces", "spaces spaces", regexMismatchError}, - {"capitalized", "Capitalized", regexMismatchError}, - {"camel case", "camelCase", regexMismatchError}, - {"invalid characters", "many/invalid$characters+in_name", regexMismatchError}, - {"starts with hyphen", "-start-with-hyphen", regexMismatchError}, - {"ends with hyphen", "end-with-hyphen-", regexMismatchError}, - {"starts with period", ".start-with-period", regexMismatchError}, - {"ends with period", "end-with-period.", regexMismatchError}, - } - - t.Parallel() - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - cl := newClient(t) - err := cl.Create(context.Background(), buildExtension(ocv1alpha1.ExtensionSpec{ - ServiceAccountName: tc.saName, - Source: ocv1alpha1.ExtensionSource{ - SourceType: ocv1alpha1.SourceTypePackage, - Package: &ocv1alpha1.ExtensionSourcePackage{ - Name: "package", - }, - }, - })) - if tc.errMsg == "" { - require.NoError(t, err, "unexpected error for serviceaccount %q: %w", tc.saName, err) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } - }) - } -} - -func TestExtensionAdmissionSource(t *testing.T) { - testCases := []struct { - name string - source ocv1alpha1.ExtensionSource - err string - }{ - {"empty source", ocv1alpha1.ExtensionSource{}, `spec.source.sourceType: Unsupported value: "": supported values: "package"`}, - {"invalid sourceType", ocv1alpha1.ExtensionSource{SourceType: "invalid"}, `spec.source.sourceType: Unsupported value: "invalid": supported values: "package"`}, - {"source with unset package", ocv1alpha1.ExtensionSource{SourceType: ocv1alpha1.SourceTypePackage}, `spec.source: Invalid value: "object": sourceType must match populated union field`}, - {"source with empty package", ocv1alpha1.ExtensionSource{SourceType: ocv1alpha1.SourceTypePackage, Package: &ocv1alpha1.ExtensionSourcePackage{}}, "spec.source.package.name in body should match"}, - {"source with minimal valid package", ocv1alpha1.ExtensionSource{SourceType: ocv1alpha1.SourceTypePackage, Package: &ocv1alpha1.ExtensionSourcePackage{Name: "package"}}, ""}, - } - - t.Parallel() - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - cl := newClient(t) - err := cl.Create(context.Background(), buildExtension(ocv1alpha1.ExtensionSpec{ - ServiceAccountName: "serviceaccount", - Source: tc.source, - })) - if tc.err == "" { - require.NoError(t, err, "unexpected error for spec.source %v: %w", tc.source, err) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.err) - } - }) - } -} - -func TestExtensionAdmissionSourcePackageName(t *testing.T) { - tooLongError := "spec.source.package.name: Too long: may not be longer than 48" - regexMismatchError := "spec.source.package.name in body should match" - - testCases := []struct { - name string - pkgName string - errMsg string - }{ - {"no package name", "", regexMismatchError}, - {"long package name", "this-is-a-really-long-package-name-that-is-greater-than-48-characters", tooLongError}, - {"leading digits with hypens", "0my-1package-9name", ""}, - {"trailing digits with hypens", "my0-package1-name9", ""}, - {"digits with hypens", "012-345-678-9", ""}, - {"letters with hypens", "abc-def-ghi-jkl", ""}, - {"letters only", "abcdefghi", ""}, - {"letters with digits", "abc123def456ghi789", ""}, - {"digits only", "1234567890", ""}, - {"single character", "a", ""}, - {"single digit", "1", ""}, - {"single hypen", "-", regexMismatchError}, - {"uppercase letters", "ABC-DEF-GHI-JKL", regexMismatchError}, - {"special characters", "my-$pecial-package-name", regexMismatchError}, - } - - t.Parallel() - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - cl := newClient(t) - err := cl.Create(context.Background(), buildExtension(ocv1alpha1.ExtensionSpec{ - ServiceAccountName: "serviceaccount", - Source: ocv1alpha1.ExtensionSource{ - SourceType: ocv1alpha1.SourceTypePackage, - Package: &ocv1alpha1.ExtensionSourcePackage{ - Name: tc.pkgName, - }, - }, - })) - if tc.errMsg == "" { - require.NoError(t, err, "unexpected error for package name %q: %w", tc.pkgName, err) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } - }) - } -} -func TestExtensionAdmissionSourcePackageVersion(t *testing.T) { - tooLongError := "spec.source.package.version: Too long: may not be longer than 64" - regexMismatchError := "spec.source.package.version in body should match" - - testCases := []struct { - name string - version string - errMsg string - }{ - {"empty semver", "", ""}, - {"simple semver", "1.2.3", ""}, - {"semver with pre-release and metadata", "1.2.3-alpha.1+metadata", ""}, - {"semver with pre-release", "1.2.3-alpha-beta", ""}, - {">= operator", ">=1.2.3", ""}, - {"=> operator", "=>1.2.3", ""}, - {">= operator with space", ">= 1.2.3", ""}, - {">= operator with 'v' prefix", ">=v1.2.3", ""}, - {">= operator with space and 'v' prefix", ">= v1.2.3", ""}, - {"<= operator", "<=1.2.3", ""}, - {"=< operator", "=<1.2.3", ""}, - {"= operator", "=1.2.3", ""}, - {"!= operator", "!=1.2.3", ""}, - {"< operator", "<1.2.3", ""}, - {"> operator", ">1.2.3", ""}, - {"~ operator", "~1.2.2", ""}, - {"~> operator", "~>1.2.3", ""}, - {"^ operator", "^1.2.3", ""}, - {"with 'v' prefix", "v1.2.3", ""}, - {"with lower-case y-stream", "1.x", ""}, - {"with upper-case Y-stream", "1.X", ""}, - {"with asterisk y-stream", "1.*", ""}, - {"with lower-case z-stream", "1.2.x", ""}, - {"with upper-case Z-stream", "1.2.X", ""}, - {"with asterisk z-stream", "1.2.*", ""}, - {"multiple operators space-separated", ">=1.2.3 <2.3.4", ""}, - {"multiple operators comma-separated", ">=1.2.3,<2.3.4", ""}, - {"multiple operators comma-and-space-separated", ">=1.2.3, <2.3.4", ""}, - {"multiple operators OR-separated", "<1.2.3||>2.3.4", ""}, - {"multiple operarors OR-and-space-separated", "<1.2.3|| >2.3.4", ""}, - {"multiple operators space-and-OR-separated", "<1.2.3 ||>2.3.4", ""}, - {"multiple operators space-and-OR-and-space-separated", "<1.2.3 || >2.3.4", ""}, - {"multiple operators with comma and OR separation", ">1.0.0,<1.2.3 || >2.1.0", ""}, - {"multiple operators with pre-release data", "<1.2.3-abc >2.3.4-def", ""}, - {"multiple operators with pre-release and metadata", "<1.2.3-abc+def >2.3.4-ghi+jkl", ""}, - // list of invalid semvers - {"invalid characters", "invalid-semver", regexMismatchError}, - {"too many components", "1.2.3.4", regexMismatchError}, - {"invalid character in pre-release", "1.2.3-beta!", regexMismatchError}, - {"invalid pre-release/4th component", "1.2.3.alpha", regexMismatchError}, - {"extra dot", "1..2.3", regexMismatchError}, - {"invalid metadata", "1.2.3-pre+bad_metadata", regexMismatchError}, - {"negative component", "1.2.-3", regexMismatchError}, - {"leading dot", ".1.2.3", regexMismatchError}, - {"invalid << operator", "<<1.2.3", regexMismatchError}, - {"invalid >> operator", ">>1.2.3", regexMismatchError}, - {"invalid >~ operator", ">~1.2.3", regexMismatchError}, - {"invalid == operator", "==1.2.3", regexMismatchError}, - {"invalid =! operator", "=!1.2.3", regexMismatchError}, - {"invalid ! operator", "!1.2.3", regexMismatchError}, - {"invalid y-stream wildcard", "1.Y", regexMismatchError}, - {"invalid AND separator", ">1.2.3 && <2.3.4", regexMismatchError}, - {"invalid semicolon separator", ">1.2.3;<2.3.4", regexMismatchError}, - {"leading zero in x-stream", "01.2.3", regexMismatchError}, - {"leading zero in y-stream", "1.02.3", regexMismatchError}, - {"leading zero in z-stream", "1.2.03", regexMismatchError}, - {"unsupported hyphen (range) operator", "1.2.3 - 2.3.4", regexMismatchError}, - {"valid semver, but too long", "1234567890.1234567890.12345678901234567890123456789012345678901234", tooLongError}, - } - - t.Parallel() - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - cl := newClient(t) - err := cl.Create(context.Background(), buildExtension(ocv1alpha1.ExtensionSpec{ - ServiceAccountName: "serviceaccount", - Source: ocv1alpha1.ExtensionSource{ - SourceType: ocv1alpha1.SourceTypePackage, - Package: &ocv1alpha1.ExtensionSourcePackage{ - Name: "package", - Version: tc.version, - }, - }, - })) - if tc.errMsg == "" { - require.NoError(t, err, "unexpected error for version %q: %w", tc.version, err) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } - }) - } -} - -func TestExtensionAdmissionSourcePackageChannel(t *testing.T) { - tooLongError := "spec.source.package.channel: Too long: may not be longer than 48" - regexMismatchError := "spec.source.package.channel in body should match" - - testCases := []struct { - name string - channelName string - errMsg string - }{ - {"no channel name", "", ""}, - {"hypen-separated", "hyphenated-name", ""}, - {"dot-separated", "dotted.name", ""}, - {"includes version", "channel-has-version-1.0.1", ""}, - {"long channel name", "longname01234567890123456789012345678901234567890", tooLongError}, - {"spaces", "spaces spaces", regexMismatchError}, - {"capitalized", "Capitalized", regexMismatchError}, - {"camel case", "camelCase", regexMismatchError}, - {"invalid characters", "many/invalid$characters+in_name", regexMismatchError}, - {"starts with hyphen", "-start-with-hyphen", regexMismatchError}, - {"ends with hyphen", "end-with-hyphen-", regexMismatchError}, - {"starts with period", ".start-with-period", regexMismatchError}, - {"ends with period", "end-with-period.", regexMismatchError}, - } - - t.Parallel() - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - cl := newClient(t) - err := cl.Create(context.Background(), buildExtension(ocv1alpha1.ExtensionSpec{ - ServiceAccountName: "serviceaccount", - Source: ocv1alpha1.ExtensionSource{ - SourceType: ocv1alpha1.SourceTypePackage, - Package: &ocv1alpha1.ExtensionSourcePackage{ - Name: "package", - Channel: tc.channelName, - }, - }, - })) - if tc.errMsg == "" { - require.NoError(t, err, "unexpected error for channel %q: %w", tc.channelName, err) - } else { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } - }) - } -} - -func buildExtension(spec ocv1alpha1.ExtensionSpec) *ocv1alpha1.Extension { - return &ocv1alpha1.Extension{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: "test-extension-", - Namespace: "default", - }, - Spec: spec, - } -} diff --git a/internal/controllers/extension_controller.go b/internal/controllers/extension_controller.go deleted file mode 100644 index 82fb22422..000000000 --- a/internal/controllers/extension_controller.go +++ /dev/null @@ -1,570 +0,0 @@ -/* -Copyright 2024. - -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 controllers - -import ( - "context" - "fmt" - "sort" - "strings" - - mmsemver "github.com/Masterminds/semver/v3" - bsemver "github.com/blang/semver/v4" - "github.com/go-logr/logr" - kappctrlv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/equality" - apierrors "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - utilerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/utils/ptr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" - "github.com/operator-framework/operator-registry/alpha/declcfg" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - "github.com/operator-framework/operator-controller/internal/catalogmetadata" - catalogfilter "github.com/operator-framework/operator-controller/internal/catalogmetadata/filter" - catalogsort "github.com/operator-framework/operator-controller/internal/catalogmetadata/sort" - "github.com/operator-framework/operator-controller/pkg/features" -) - -// ExtensionReconciler reconciles a Extension object -type ExtensionReconciler struct { - client.Client - BundleProvider BundleProvider -} - -var ( - bundleVersionKey = "olm.operatorframework.io/bundleVersion" -) - -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions/status,verbs=update;patch -//+kubebuilder:rbac:groups=olm.operatorframework.io,resources=extensions/finalizers,verbs=update -//+kubebuilder:rbac:groups=kappctrl.k14s.io,resources=apps,verbs=get;list;watch;create;update;patch - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -func (r *ExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - l := log.FromContext(ctx).WithName("extension-controller") - l.V(1).Info("starting") - defer l.V(1).Info("ending") - - var existingExt = &ocv1alpha1.Extension{} - if err := r.Client.Get(ctx, req.NamespacedName, existingExt); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - reconciledExt := existingExt.DeepCopy() - res, reconcileErr := r.reconcile(ctx, reconciledExt) - - // Do checks before any Update()s, as Update() may modify the resource structure! - updateStatus := !equality.Semantic.DeepEqual(existingExt.Status, reconciledExt.Status) - updateFinalizers := !equality.Semantic.DeepEqual(existingExt.Finalizers, reconciledExt.Finalizers) - unexpectedFieldsChanged := r.checkForUnexpectedFieldChange(*existingExt, *reconciledExt) - - if updateStatus { - if updateErr := r.Client.Status().Update(ctx, reconciledExt); updateErr != nil { - return res, utilerrors.NewAggregate([]error{reconcileErr, updateErr}) - } - } - - if unexpectedFieldsChanged { - panic("spec or metadata changed by reconciler") - } - - if updateFinalizers { - if updateErr := r.Client.Update(ctx, reconciledExt); updateErr != nil { - return res, utilerrors.NewAggregate([]error{reconcileErr, updateErr}) - } - } - - return res, reconcileErr -} - -// Compare resources - ignoring status & metadata.finalizers -func (*ExtensionReconciler) checkForUnexpectedFieldChange(a, b ocv1alpha1.Extension) bool { - a.Status, b.Status = ocv1alpha1.ExtensionStatus{}, ocv1alpha1.ExtensionStatus{} - a.Finalizers, b.Finalizers = []string{}, []string{} - return !equality.Semantic.DeepEqual(a, b) -} - -// Helper function to do the actual reconcile -// -// Today we always return ctrl.Result{} and an error. -// But in the future we might update this function -// to return different results (e.g. requeue). -// -//nolint:unparam -func (r *ExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alpha1.Extension) (ctrl.Result, error) { - l := log.FromContext(ctx).WithName("extension-controller") - - // Don't do anything if feature gated - if !features.OperatorControllerFeatureGate.Enabled(features.EnableExtensionAPI) { - l.Info("extension feature is gated", "name", ext.GetName(), "namespace", ext.GetNamespace()) - - // Set the TypeInstalled condition to Failed to indicate that the resolution - // hasn't been attempted yet, due to the spec being invalid. - ext.Status.InstalledBundle = nil - setInstalledStatusConditionFailed(&ext.Status.Conditions, "extension feature is disabled", ext.GetGeneration()) - // Set the TypeResolved condition to Failed to indicate that the resolution - // hasn't been attempted yet, due to the spec being invalid. - ext.Status.ResolvedBundle = nil - setResolvedStatusConditionFailed(&ext.Status.Conditions, "extension feature is disabled", ext.GetGeneration()) - - setDeprecationStatusesUnknown(&ext.Status.Conditions, "extension feature is disabled", ext.GetGeneration()) - return ctrl.Result{}, nil - } - - // Don't do anything if Paused - ext.Status.Paused = ext.Spec.Paused - if ext.Spec.Paused { - l.Info("resource is paused", "name", ext.GetName(), "namespace", ext.GetNamespace()) - return ctrl.Result{}, nil - } - - // TODO: Improve the resolution logic. - bundle, err := r.resolve(ctx, *ext) - if err != nil { - if c := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1alpha1.TypeInstalled); c == nil { - ext.Status.InstalledBundle = nil - setInstalledStatusConditionFailed(&ext.Status.Conditions, "installation has not been attempted as resolution failed", ext.GetGeneration()) - } - ext.Status.ResolvedBundle = nil - setResolvedStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) - setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as resolution failed", ext.GetGeneration()) - return ctrl.Result{}, err - } - - // Now we can set the Resolved Condition, and the resolvedBundleSource field to the bundle.Image value. - ext.Status.ResolvedBundle = bundleMetadataFor(bundle) - setResolvedStatusConditionSuccess(&ext.Status.Conditions, fmt.Sprintf("resolved to %q", bundle.Image), ext.GetGeneration()) - - // Right now, we just assume that the bundle is a plain+v0 bundle. - app, err := r.GenerateExpectedApp(*ext, bundle) - if err != nil { - if c := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1alpha1.TypeInstalled); c == nil { - ext.Status.InstalledBundle = nil - setInstalledStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) - setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) - } - setProgressingStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) - return ctrl.Result{}, err - } - - if err := r.ensureApp(ctx, app); err != nil { - // originally Reason: ocv1alpha1.ReasonInstallationFailed - ext.Status.InstalledBundle = nil - setInstalledStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) - setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) - setProgressingStatusConditionProgressing(&ext.Status.Conditions, "installation failed", ext.GetGeneration()) - return ctrl.Result{}, err - } - - // Converting into structured so that we can map the relevant status to Extension. - existingTypedApp := &kappctrlv1alpha1.App{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(app.UnstructuredContent(), existingTypedApp); err != nil { - // originally Reason: ocv1alpha1.ReasonInstallationStatusUnknown - ext.Status.InstalledBundle = nil - setInstalledStatusConditionFailed(&ext.Status.Conditions, err.Error(), ext.GetGeneration()) - setDeprecationStatusesUnknown(&ext.Status.Conditions, "deprecation checks have not been attempted as installation has failed", ext.GetGeneration()) - setProgressingStatusConditionProgressing(&ext.Status.Conditions, "installation failed", ext.GetGeneration()) - return ctrl.Result{}, err - } - - ext.Status.InstalledBundle = bundleMetadataFor(bundle) - setInstalledStatusConditionSuccess(&ext.Status.Conditions, fmt.Sprintf("successfully installed %v", ext.Status.InstalledBundle), ext.GetGeneration()) - SetDeprecationStatusInExtension(ext, bundle) - - // TODO: add conditions to determine extension health - mapAppStatusToCondition(existingTypedApp, ext) - - return ctrl.Result{}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *ExtensionReconciler) SetupWithManager(mgr ctrl.Manager) error { - // TODO: Add watch for kapp-controller resources - - // When feature-gated, don't watch catalogs. - if !features.OperatorControllerFeatureGate.Enabled(features.EnableExtensionAPI) { - return ctrl.NewControllerManagedBy(mgr). - For(&ocv1alpha1.Extension{}). - Complete(r) - } - - return ctrl.NewControllerManagedBy(mgr). - For(&ocv1alpha1.Extension{}). - Owns(&kappctrlv1alpha1.App{}). - Watches(&catalogd.Catalog{}, handler.EnqueueRequestsFromMapFunc(extensionRequestsForCatalog(mgr.GetClient(), mgr.GetLogger()))). - Complete(r) -} - -// mapAppStatusToCondition maps the reconciling/deleting App conditions to the installed/deleting conditions on the Extension. -func mapAppStatusToCondition(existingApp *kappctrlv1alpha1.App, ext *ocv1alpha1.Extension) { - // Note: App.Status.Inspect errors are never surfaced to App conditions, so are currently ignored when determining App status. - if ext == nil || existingApp == nil { - return - } - message := existingApp.Status.FriendlyDescription - if len(message) == 0 || strings.Contains(message, "Error (see .status.usefulErrorMessage for details)") { - message = existingApp.Status.UsefulErrorMessage - } - - appStatusMapFn := map[kappctrlv1alpha1.ConditionType]func(*[]metav1.Condition, string, int64){ - kappctrlv1alpha1.Deleting: setProgressingStatusConditionProgressing, - kappctrlv1alpha1.Reconciling: setProgressingStatusConditionProgressing, - kappctrlv1alpha1.DeleteFailed: setProgressingStatusConditionFailed, - kappctrlv1alpha1.ReconcileFailed: setProgressingStatusConditionFailed, - kappctrlv1alpha1.ReconcileSucceeded: setProgressingStatusConditionSuccess, - } - for cond := range appStatusMapFn { - if c := findStatusCondition(existingApp.Status.GenericStatus.Conditions, cond); c != nil && c.Status == corev1.ConditionTrue { - if len(message) == 0 { - message = c.Message - } - appStatusMapFn[cond](&ext.Status.Conditions, fmt.Sprintf("App %s: %s", c.Type, message), ext.Generation) - return - } - } - if len(message) == 0 { - message = "waiting for app" - } - setProgressingStatusConditionProgressing(&ext.Status.Conditions, message, ext.Generation) -} - -// setDeprecationStatus will set the appropriate deprecation statuses for a Extension -// based on the provided bundle -func SetDeprecationStatusInExtension(ext *ocv1alpha1.Extension, bundle *catalogmetadata.Bundle) { - // reset conditions to false - conditionTypes := []string{ - ocv1alpha1.TypeDeprecated, - ocv1alpha1.TypePackageDeprecated, - ocv1alpha1.TypeChannelDeprecated, - ocv1alpha1.TypeBundleDeprecated, - } - - for _, conditionType := range conditionTypes { - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: conditionType, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionFalse, - Message: "", - ObservedGeneration: ext.Generation, - }) - } - - // There are two early return scenarios here: - // 1) The bundle is not deprecated (i.e bundle deprecations) - // AND there are no other deprecations associated with the bundle - // 2) The bundle is not deprecated, there are deprecations associated - // with the bundle (i.e at least one channel the bundle is present in is deprecated OR whole package is deprecated), - // and the Extension does not specify a channel. This is because the channel deprecations - // are a loose deprecation coupling on the bundle. A Extension installation is only - // considered deprecated by a channel deprecation when a deprecated channel is specified via - // the spec.channel field. - if (!bundle.IsDeprecated() && !bundle.HasDeprecation()) || (!bundle.IsDeprecated() && ext.Spec.Source.Package.Channel == "") { - return - } - - deprecationMessages := []string{} - - for _, deprecation := range bundle.Deprecations { - switch deprecation.Reference.Schema { - case declcfg.SchemaPackage: - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: ocv1alpha1.TypePackageDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - Message: deprecation.Message, - ObservedGeneration: ext.Generation, - }) - case declcfg.SchemaChannel: - if ext.Spec.Source.Package.Channel != deprecation.Reference.Name { - continue - } - - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: ocv1alpha1.TypeChannelDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - Message: deprecation.Message, - ObservedGeneration: ext.Generation, - }) - case declcfg.SchemaBundle: - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: ocv1alpha1.TypeBundleDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - Message: deprecation.Message, - ObservedGeneration: ext.Generation, - }) - } - - deprecationMessages = append(deprecationMessages, deprecation.Message) - } - - if len(deprecationMessages) > 0 { - apimeta.SetStatusCondition(&ext.Status.Conditions, metav1.Condition{ - Type: ocv1alpha1.TypeDeprecated, - Reason: ocv1alpha1.ReasonDeprecated, - Status: metav1.ConditionTrue, - Message: strings.Join(deprecationMessages, ";"), - ObservedGeneration: ext.Generation, - }) - } -} - -// findStatusCondition finds the conditionType in conditions. -// TODO: suggest using upstream conditions to Carvel. -func findStatusCondition(conditions []kappctrlv1alpha1.Condition, conditionType kappctrlv1alpha1.ConditionType) *kappctrlv1alpha1.Condition { - for i := range conditions { - if conditions[i].Type == conditionType { - return &conditions[i] - } - } - return nil -} - -func (r *ExtensionReconciler) ensureApp(ctx context.Context, desiredApp *unstructured.Unstructured) error { - existingApp, err := r.existingAppUnstructured(ctx, desiredApp.GetName(), desiredApp.GetNamespace()) - if client.IgnoreNotFound(err) != nil { - return err - } - - // If the existing App already has everything that the desired App has, no need to contact the API server. - // Make sure the status of the existingApp from the server is as expected. - if equality.Semantic.DeepDerivative(desiredApp, existingApp) { - *desiredApp = *existingApp - return nil - } - - return r.Client.Patch(ctx, desiredApp, client.Apply, client.ForceOwnership, client.FieldOwner("operator-controller")) -} - -func (r *ExtensionReconciler) existingAppUnstructured(ctx context.Context, name, namespace string) (*unstructured.Unstructured, error) { - existingApp := &kappctrlv1alpha1.App{} - err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, existingApp) - if err != nil { - return nil, err - } - existingApp.APIVersion = "kappctrl.k14s.io/v1alpha1" - existingApp.Kind = "App" - unstrExistingAppObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(existingApp) - if err != nil { - return nil, err - } - return &unstructured.Unstructured{Object: unstrExistingAppObj}, nil -} - -// Generate reconcile requests for all extensions affected by a catalog change -func extensionRequestsForCatalog(c client.Reader, logger logr.Logger) handler.MapFunc { - return func(ctx context.Context, _ client.Object) []reconcile.Request { - // no way of associating an extension to a catalog so create reconcile requests for everything - extensions := ocv1alpha1.ExtensionList{} - err := c.List(ctx, &extensions) - if err != nil { - logger.Error(err, "unable to enqueue extensions for catalog reconcile") - return nil - } - var requests []reconcile.Request - for _, ext := range extensions.Items { - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: ext.GetNamespace(), - Name: ext.GetName(), - }, - }) - } - return requests - } -} - -func (r *ExtensionReconciler) GenerateExpectedApp(o ocv1alpha1.Extension, bundle *catalogmetadata.Bundle) (*unstructured.Unstructured, error) { - bundleVersion, err := bundle.Version() - if err != nil { - return nil, fmt.Errorf("failed to generate App from Extension %q with bundle %q: %w", o.GetName(), bundle.Name, err) - } - bundlePath := bundle.Image - - // We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver. - // If you use a typed object, any default values from that struct get serialized into the JSON patch, which could - // cause unrelated fields to be patched back to the default value even though that isn't the intention. Using an - // unstructured ensures that the patch contains only what is specified. Using unstructured like this is basically - // identical to "kubectl apply -f" - spec := map[string]interface{}{ - "serviceAccountName": o.Spec.ServiceAccountName, - "fetch": []interface{}{ - map[string]interface{}{ - "image": map[string]interface{}{ - "url": bundlePath, - }, - }, - }, - "template": []interface{}{ - map[string]interface{}{ - "ytt": map[string]interface{}{}, - }, - }, - "deploy": []interface{}{ - map[string]interface{}{ - "kapp": map[string]interface{}{}, - }, - }, - } - - app := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "kappctrl.k14s.io/v1alpha1", - "kind": "App", - "metadata": map[string]interface{}{ - "name": o.GetName(), - "namespace": o.GetNamespace(), - "annotations": map[string]string{ - bundleVersionKey: bundleVersion.String(), - }, - }, - "spec": spec, - }, - } - - app.SetOwnerReferences([]metav1.OwnerReference{ - { - APIVersion: ocv1alpha1.GroupVersion.String(), - Kind: "Extension", - Name: o.Name, - UID: o.UID, - Controller: ptr.To(true), - BlockOwnerDeletion: ptr.To(true), - }, - }) - return app, nil -} - -func (r *ExtensionReconciler) getInstalledVersion(ctx context.Context, namespacedName types.NamespacedName) (*bsemver.Version, error) { - existingApp, err := r.existingAppUnstructured(ctx, namespacedName.Name, namespacedName.Namespace) - if err != nil { - return nil, err - } - existingVersion, ok := existingApp.GetAnnotations()[bundleVersionKey] - if !ok { - return nil, fmt.Errorf("existing App %q in Namespace %q missing bundle version", namespacedName.Name, namespacedName.Namespace) - } - - existingVersionSemver, err := bsemver.New(existingVersion) - if err != nil { - return nil, fmt.Errorf("could not determine bundle version of existing App %q in Namespace %q: %w", namespacedName.Name, namespacedName.Namespace, err) - } - return existingVersionSemver, nil -} - -func (r *ExtensionReconciler) resolve(ctx context.Context, extension ocv1alpha1.Extension) (*catalogmetadata.Bundle, error) { - allBundles, err := r.BundleProvider.Bundles(ctx) - if err != nil { - return nil, err - } - - packageName := extension.Spec.Source.Package.Name - channelName := extension.Spec.Source.Package.Channel - versionRange := extension.Spec.Source.Package.Version - - predicates := []catalogfilter.Predicate[catalogmetadata.Bundle]{ - catalogfilter.WithPackageName(packageName), - } - - if channelName != "" { - predicates = append(predicates, catalogfilter.InChannel(channelName)) - } - - if versionRange != "" { - vr, err := mmsemver.NewConstraint(versionRange) - if err != nil { - return nil, fmt.Errorf("invalid version range %q: %w", versionRange, err) - } - predicates = append(predicates, catalogfilter.InMastermindsSemverRange(vr)) - } - - var installedVersion string - // Do not include bundle versions older than currently installed unless UpgradeConstraintPolicy = 'Ignore' - if extension.Spec.Source.Package.UpgradeConstraintPolicy != ocv1alpha1.UpgradeConstraintPolicyIgnore { - installedVersionSemver, err := r.getInstalledVersion(ctx, types.NamespacedName{Name: extension.GetName(), Namespace: extension.GetNamespace()}) - if err != nil && !apierrors.IsNotFound(err) { - return nil, err - } - if installedVersionSemver != nil { - installedVersion = installedVersionSemver.String() - - // Based on installed version create a caret range comparison constraint - // to allow only minor and patch version as successors. - wantedVersionRangeConstraint, err := mmsemver.NewConstraint(fmt.Sprintf("^%s", installedVersion)) - if err != nil { - return nil, err - } - predicates = append(predicates, catalogfilter.InMastermindsSemverRange(wantedVersionRangeConstraint)) - } - } - - resultSet := catalogfilter.Filter(allBundles, catalogfilter.And(predicates...)) - - if len(resultSet) == 0 { - var versionError, channelError, existingVersionError string - if versionRange != "" { - versionError = fmt.Sprintf(" matching version %q", versionRange) - } - if channelName != "" { - channelError = fmt.Sprintf(" in channel %q", channelName) - } - if installedVersion != "" { - existingVersionError = fmt.Sprintf(" which upgrades currently installed version %q", installedVersion) - } - return nil, fmt.Errorf("no package %q%s%s%s found", packageName, versionError, channelError, existingVersionError) - } - - sort.SliceStable(resultSet, func(i, j int) bool { - return catalogsort.ByVersion(resultSet[i], resultSet[j]) - }) - sort.SliceStable(resultSet, func(i, j int) bool { - return catalogsort.ByDeprecated(resultSet[i], resultSet[j]) - }) - return resultSet[0], nil -} - -// bundleMetadataFor returns a BundleMetadata for the given bundle. If the provided bundle is nil, -// this function panics. It is up to the caller to ensure that the bundle is non-nil. -func bundleMetadataFor(bundle *catalogmetadata.Bundle) *ocv1alpha1.BundleMetadata { - if bundle == nil { - panic("programmer error: provided bundle must be non-nil to create BundleMetadata") - } - ver, err := bundle.Version() - if err != nil { - ver = &bsemver.Version{} - } - return &ocv1alpha1.BundleMetadata{ - Name: bundle.Name, - Version: ver.String(), - } -} diff --git a/internal/controllers/extension_controller_test.go b/internal/controllers/extension_controller_test.go deleted file mode 100644 index 331b4943e..000000000 --- a/internal/controllers/extension_controller_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package controllers_test - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - carvelv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/rand" - featuregatetesting "k8s.io/component-base/featuregate/testing" - ctrl "sigs.k8s.io/controller-runtime" - - ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - "github.com/operator-framework/operator-controller/internal/conditionsets" - "github.com/operator-framework/operator-controller/pkg/features" -) - -const ( - testServiceAccount = "test-sa" -) - -// Describe: Extension Controller Test -func TestExtensionDoesNotExist(t *testing.T) { - _, reconciler := newClientAndExtensionReconciler(t) - - t.Log("When the extension does not exist") - t.Log("It returns no error") - res, err := reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: types.NamespacedName{Name: "non-existent", Namespace: "non-existent"}}) - require.Equal(t, ctrl.Result{}, res) - require.NoError(t, err) -} - -func TestExtensionReconcile(t *testing.T) { - t.Skip("Skipping this till kapp-controller test setup is implemented.") - c, reconciler := newClientAndExtensionReconciler(t) - ctx := context.Background() - - testCases := []struct { - name string - featureGateEnabled bool - paused bool - assert func(*testing.T, ctrl.Result, error, *ocv1alpha1.Extension) - }{ - {"feature gate disabled", false, false, func(t *testing.T, res ctrl.Result, err error, ext *ocv1alpha1.Extension) { - assert.Equal(t, ctrl.Result{}, res) - assert.NoError(t, err) - verifyExtensionInvariants(t, ext) - assert.Empty(t, ext.Status.InstalledBundle) - assert.Empty(t, ext.Status.ResolvedBundle) - for _, cond := range ext.Status.Conditions { - assert.Equal(t, metav1.ConditionUnknown, cond.Status) - assert.Equal(t, "extension feature is disabled", cond.Message) - } - }}, - {"feature gate enabled and paused", true, true, func(t *testing.T, res ctrl.Result, err error, ext *ocv1alpha1.Extension) { - assert.Equal(t, ctrl.Result{}, res) - assert.NoError(t, err) - assert.Equal(t, ocv1alpha1.ExtensionStatus{Paused: true}, ext.Status) - }}, - {"feature gate enabled and active", true, false, func(t *testing.T, res ctrl.Result, err error, ext *ocv1alpha1.Extension) { - assert.Equal(t, ctrl.Result{}, res) - assert.NoError(t, err) - verifyExtensionInvariants(t, ext) - assert.False(t, ext.Status.Paused) - assert.Empty(t, ext.Status.InstalledBundle) - assert.Empty(t, ext.Status.ResolvedBundle) - for _, cond := range ext.Status.Conditions { - assert.Equal(t, metav1.ConditionUnknown, cond.Status) - assert.Equal(t, "the Extension interface is not fully implemented", cond.Message) - } - }}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - extKey := types.NamespacedName{Name: fmt.Sprintf("extension-test-%s", rand.String(8)), Namespace: "default"} - ext := &ocv1alpha1.Extension{ - ObjectMeta: metav1.ObjectMeta{Name: extKey.Name, Namespace: extKey.Namespace}, - Spec: ocv1alpha1.ExtensionSpec{ - Paused: tc.paused, - ServiceAccountName: "test-service-account", - Source: ocv1alpha1.ExtensionSource{SourceType: ocv1alpha1.SourceTypePackage, Package: &ocv1alpha1.ExtensionSourcePackage{Name: "test-package"}}, - }, - } - require.NoError(t, c.Create(ctx, ext)) - - defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.EnableExtensionAPI, tc.featureGateEnabled)() - res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey}) - - require.NoError(t, c.Get(ctx, extKey, ext)) - tc.assert(t, res, err, ext) - }) - } -} - -func TestExtensionResolve(t *testing.T) { - defer featuregatetesting.SetFeatureGateDuringTest(t, features.OperatorControllerFeatureGate, features.EnableExtensionAPI, true)() - ctx := context.Background() - - testCases := []struct { - name string - packageName string - packageVersion string - upgradeConstraintPolicy ocv1alpha1.UpgradeConstraintPolicy - wantErr error - existingApp *carvelv1alpha1.App - wantCondition metav1.Condition - }{ - { - name: "basic install with specified version", - packageName: "prometheus", - packageVersion: "0.37.0", - wantCondition: metav1.Condition{ - Status: metav1.ConditionTrue, - Message: `resolved to "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`, - }, - }, - { - name: "existing App of same version", - packageName: "prometheus", - packageVersion: "0.37.0", - existingApp: &carvelv1alpha1.App{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Annotations: map[string]string{ - "olm.operatorframework.io/bundleVersion": "0.37.0", - }, - }, - Spec: carvelv1alpha1.AppSpec{}, - }, - wantCondition: metav1.Condition{ - Status: metav1.ConditionTrue, - Message: `resolved to "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`, - }, - }, - { - name: "existing App of higher version than requested", - packageName: "prometheus", - packageVersion: "0.37.0", - existingApp: &carvelv1alpha1.App{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Annotations: map[string]string{ - "olm.operatorframework.io/bundleVersion": "0.38.0", - }, - }, - Spec: carvelv1alpha1.AppSpec{}, - }, - wantErr: fmt.Errorf("no package \"prometheus\" matching version \"0.37.0\" which upgrades currently installed version \"0.38.0\" found"), - wantCondition: metav1.Condition{ - Status: metav1.ConditionFalse, - Message: `no package "prometheus" matching version "0.37.0" which upgrades currently installed version "0.38.0" found`, - }, - }, - { - name: "downgrade with UpgradeConstraintPolicy of 'Ignore'", - packageName: "prometheus", - packageVersion: "0.37.0", - upgradeConstraintPolicy: ocv1alpha1.UpgradeConstraintPolicyIgnore, - existingApp: &carvelv1alpha1.App{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Annotations: map[string]string{ - "olm.operatorframework.io/bundleVersion": "0.38.0", - }, - }, - Spec: carvelv1alpha1.AppSpec{}, - }, - wantCondition: metav1.Condition{ - Status: metav1.ConditionTrue, - Message: `resolved to "quay.io/operatorhubio/prometheus@sha256:3e281e587de3d03011440685fc4fb782672beab044c1ebadc42788ce05a21c35"`, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - c, reconciler := newClientAndExtensionReconciler(t) - extName := fmt.Sprintf("extension-test-%s", rand.String(8)) - ext := &ocv1alpha1.Extension{ - ObjectMeta: metav1.ObjectMeta{Name: extName, Namespace: "default"}, - Spec: ocv1alpha1.ExtensionSpec{ - ServiceAccountName: testServiceAccount, - Source: ocv1alpha1.ExtensionSource{ - SourceType: ocv1alpha1.SourceTypePackage, - Package: &ocv1alpha1.ExtensionSourcePackage{ - Name: tc.packageName, - Version: tc.packageVersion, - UpgradeConstraintPolicy: tc.upgradeConstraintPolicy, - }, - }, - }, - } - if tc.existingApp != nil { - tc.existingApp.Name = extName - require.NoError(t, c.Create(ctx, tc.existingApp)) - } - - require.NoError(t, c.Create(ctx, ext)) - extNN := types.NamespacedName{Name: ext.GetName(), Namespace: ext.GetNamespace()} - - res, reconcileErr := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extNN}) - assert.Equal(t, tc.wantErr, reconcileErr) - if tc.wantErr != nil { - err := c.Get(ctx, extNN, ext) - assert.NoError(t, err) - - assert.Equal(t, ctrl.Result{}, res) - - condition := apimeta.FindStatusCondition(ext.Status.Conditions, ocv1alpha1.TypeResolved) - assert.NotNil(t, condition) - assert.Equal(t, tc.wantCondition.Status, condition.Status) - assert.Equal(t, tc.wantCondition.Message, condition.Message) - } - }) - } -} - -func verifyExtensionInvariants(t *testing.T, ext *ocv1alpha1.Extension) { - verifyExtensionConditionsInvariants(t, ext) -} - -func verifyExtensionConditionsInvariants(t *testing.T, ext *ocv1alpha1.Extension) { - // Expect that the extension's set of conditions contains all defined - // condition types for the Extension API. Every reconcile should always - // ensure every condition type's status/reason/message reflects the state - // read during _this_ reconcile call. - require.Len(t, ext.Status.Conditions, len(conditionsets.ConditionTypes)) - for _, tt := range conditionsets.ConditionTypes { - cond := apimeta.FindStatusCondition(ext.Status.Conditions, tt) - require.NotNil(t, cond) - require.NotEmpty(t, cond.Status) - require.Contains(t, conditionsets.ConditionReasons, cond.Reason) - require.Equal(t, ext.GetGeneration(), cond.ObservedGeneration) - } -} diff --git a/internal/controllers/suite_test.go b/internal/controllers/suite_test.go index bb71f0a4f..afa5746c3 100644 --- a/internal/controllers/suite_test.go +++ b/internal/controllers/suite_test.go @@ -50,16 +50,6 @@ func newClientAndReconciler(t *testing.T) (client.Client, *controllers.ClusterEx return cl, reconciler } -func newClientAndExtensionReconciler(t *testing.T) (client.Client, *controllers.ExtensionReconciler) { - cl := newClient(t) - fakeCatalogClient := testutil.NewFakeCatalogClient(testBundleList) - reconciler := &controllers.ExtensionReconciler{ - Client: cl, - BundleProvider: &fakeCatalogClient, - } - return cl, reconciler -} - var ( cfg *rest.Config ) diff --git a/pkg/scheme/scheme.go b/pkg/scheme/scheme.go index e326aec89..afc179dc3 100644 --- a/pkg/scheme/scheme.go +++ b/pkg/scheme/scheme.go @@ -1,7 +1,6 @@ package scheme import ( - carvelv1alpha1 "github.com/vmware-tanzu/carvel-kapp-controller/pkg/apis/kappctrl/v1alpha1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -21,7 +20,6 @@ func init() { utilruntime.Must(ocv1alpha1.AddToScheme(Scheme)) utilruntime.Must(rukpakv1alpha2.AddToScheme(Scheme)) utilruntime.Must(catalogd.AddToScheme(Scheme)) - utilruntime.Must(carvelv1alpha1.AddToScheme(Scheme)) utilruntime.Must(appsv1.AddToScheme(Scheme)) utilruntime.Must(corev1.AddToScheme(Scheme)) //+kubebuilder:scaffold:scheme diff --git a/scripts/install.tpl.sh b/scripts/install.tpl.sh index b98f47bec..e6d4be2a6 100644 --- a/scripts/install.tpl.sh +++ b/scripts/install.tpl.sh @@ -12,9 +12,8 @@ fi catalogd_version=$CATALOGD_VERSION cert_mgr_version=$CERT_MGR_VERSION rukpak_version=$RUKPAK_VERSION -kapp_version=$KAPP_VERSION -if [[ -z "$catalogd_version" || -z "$cert_mgr_version" || -z "$rukpak_version" || -z "$kapp_version" ]]; then +if [[ -z "$catalogd_version" || -z "$cert_mgr_version" || -z "$rukpak_version" ]]; then err="Error: Missing component version(s) for: " if [[ -z "$catalogd_version" ]]; then err+="catalogd " @@ -25,9 +24,6 @@ if [[ -z "$catalogd_version" || -z "$cert_mgr_version" || -z "$rukpak_version" | if [[ -z "$rukpak_version" ]]; then err+="rukpak " fi - if [[ -z "$kapp_version" ]]; then - err+="kapp " - fi echo "$err" exit 1 fi @@ -43,9 +39,6 @@ function kubectl_wait() { kubectl apply -f "https://github.com/cert-manager/cert-manager/releases/download/${cert_mgr_version}/cert-manager.yaml" kubectl_wait "cert-manager" "deployment/cert-manager-webhook" "60s" -kubectl apply -f "https://github.com/carvel-dev/kapp-controller/releases/download/${kapp_version}/release.yml" -kubectl_wait "kapp-controller" "deployment.apps/kapp-controller" 60s - kubectl apply -f "https://github.com/operator-framework/rukpak/releases/download/${rukpak_version}/rukpak.yaml" kubectl_wait "rukpak-system" "deployment/core" "60s" kubectl_wait "rukpak-system" "deployment/helm-provisioner" "60s" diff --git a/testdata/crds/kappctrl.k14s.io_app.yaml b/testdata/crds/kappctrl.k14s.io_app.yaml deleted file mode 100644 index a3b3f987d..000000000 --- a/testdata/crds/kappctrl.k14s.io_app.yaml +++ /dev/null @@ -1,728 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: apps.kappctrl.k14s.io -spec: - group: kappctrl.k14s.io - names: - categories: - - carvel - kind: App - listKind: AppList - plural: apps - singular: app - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: Friendly description - jsonPath: .status.friendlyDescription - name: Description - type: string - - description: Last time app started being deployed. Does not mean anything was changed. - jsonPath: .status.deploy.startedAt - name: Since-Deploy - type: date - - description: Time since creation - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: 'An App is a set of Kubernetes resources. These resources could span any number of namespaces or could be cluster-wide (e.g. CRDs). An App is represented in kapp-controller using a App CR. The App CR comprises of three main sections: spec.fetch – declare source for fetching configuration and OCI images spec.template – declare templating tool and values spec.deploy – declare deployment tool and any deploy specific configuration' - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - properties: - canceled: - description: Cancels current and future reconciliations (optional; default=false) - type: boolean - cluster: - description: Specifies that app should be deployed to destination cluster; by default, cluster is same as where this resource resides (optional; v0.5.0+) - properties: - kubeconfigSecretRef: - description: Specifies secret containing kubeconfig (required) - properties: - key: - description: Specifies key that contains kubeconfig (optional) - type: string - name: - description: Specifies secret name within app's namespace (required) - type: string - type: object - namespace: - description: Specifies namespace in destination cluster (optional) - type: string - type: object - defaultNamespace: - description: Specifies the default namespace to install the App resources, by default this is same as the App's namespace (optional; v0.48.0+) - type: string - deploy: - items: - properties: - kapp: - description: Use kapp to deploy resources - properties: - delete: - description: Configuration for delete command (optional) - properties: - rawOptions: - description: Pass through options to kapp delete (optional) - items: - type: string - type: array - type: object - inspect: - description: 'Configuration for inspect command (optional) as of kapp-controller v0.31.0, inspect is disabled by default add rawOptions or use an empty inspect config like `inspect: {}` to enable' - properties: - rawOptions: - description: Pass through options to kapp inspect (optional) - items: - type: string - type: array - type: object - intoNs: - description: Override namespace for all resources (optional) - type: string - mapNs: - description: Provide custom namespace override mapping (optional) - items: - type: string - type: array - rawOptions: - description: Pass through options to kapp deploy (optional) - items: - type: string - type: array - type: object - type: object - type: array - fetch: - items: - properties: - git: - description: Uses git to clone repository - properties: - forceHTTPBasicAuth: - description: Force the usage of HTTP Basic Auth when Basic Auth is provided (optional) - type: boolean - lfsSkipSmudge: - description: Skip lfs download (optional) - type: boolean - ref: - description: Branch, tag, commit; origin is the name of the remote (optional) - type: string - refSelection: - description: Specifies a strategy to resolve to an explicit ref (optional; v0.24.0+) - properties: - semver: - properties: - constraints: - type: string - prereleases: - properties: - identifiers: - items: - type: string - type: array - type: object - type: object - type: object - secretRef: - description: 'Secret with auth details. allowed keys: ssh-privatekey, ssh-knownhosts, username, password (optional) (if ssh-knownhosts is not specified, git will not perform strict host checking)' - properties: - name: - description: Object is expected to be within same namespace - type: string - type: object - subPath: - description: Grab only portion of repository (optional) - type: string - url: - description: http or ssh urls are supported (required) - type: string - type: object - helmChart: - description: Uses helm fetch to fetch specified chart - properties: - name: - description: 'Example: stable/redis' - type: string - repository: - properties: - secretRef: - properties: - name: - description: Object is expected to be within same namespace - type: string - type: object - url: - description: Repository url; scheme of oci:// will fetch experimental helm oci chart (v0.19.0+) (required) - type: string - type: object - version: - type: string - type: object - http: - description: Uses http library to fetch file - properties: - secretRef: - description: 'Secret to provide auth details (optional) Secret may include one or more keys: username, password' - properties: - name: - description: Object is expected to be within same namespace - type: string - type: object - sha256: - description: Checksum to verify after download (optional) - type: string - subPath: - description: Grab only portion of download (optional) - type: string - url: - description: 'URL can point to one of following formats: text, tgz, zip http and https url are supported; plain file, tgz and tar types are supported (required)' - type: string - type: object - image: - description: Pulls content from Docker/OCI registry - properties: - secretRef: - description: 'Secret may include one or more keys: username, password, token. By default anonymous access is used for authentication.' - properties: - name: - description: Object is expected to be within same namespace - type: string - type: object - subPath: - description: Grab only portion of image (optional) - type: string - tagSelection: - description: Specifies a strategy to choose a tag (optional; v0.24.0+) if specified, do not include a tag in url key - properties: - semver: - properties: - constraints: - type: string - prereleases: - properties: - identifiers: - items: - type: string - type: array - type: object - type: object - type: object - url: - description: 'Docker image url; unqualified, tagged, or digest references supported (required) Example: username/app1-config:v0.1.0' - type: string - type: object - imgpkgBundle: - description: Pulls imgpkg bundle from Docker/OCI registry (v0.17.0+) - properties: - image: - description: Docker image url; unqualified, tagged, or digest references supported (required) - type: string - secretRef: - description: 'Secret may include one or more keys: username, password, token. By default anonymous access is used for authentication.' - properties: - name: - description: Object is expected to be within same namespace - type: string - type: object - tagSelection: - description: Specifies a strategy to choose a tag (optional; v0.24.0+) if specified, do not include a tag in url key - properties: - semver: - properties: - constraints: - type: string - prereleases: - properties: - identifiers: - items: - type: string - type: array - type: object - type: object - type: object - type: object - inline: - description: Pulls content from within this resource; or other resources in the cluster - properties: - paths: - additionalProperties: - type: string - description: Specifies mapping of paths to their content; not recommended for sensitive values as CR is not encrypted (optional) - type: object - pathsFrom: - description: Specifies content via secrets and config maps; data values are recommended to be placed in secrets (optional) - items: - properties: - configMapRef: - properties: - directoryPath: - description: Specifies where to place files found in secret (optional) - type: string - name: - type: string - type: object - secretRef: - properties: - directoryPath: - description: Specifies where to place files found in secret (optional) - type: string - name: - type: string - type: object - type: object - type: array - type: object - path: - description: Relative path to place the fetched artifacts - type: string - type: object - type: array - noopDelete: - description: Deletion requests for the App will result in the App CR being deleted, but its associated resources will not be deleted (optional; default=false; v0.18.0+) - type: boolean - paused: - description: Pauses _future_ reconciliation; does _not_ affect currently running reconciliation (optional; default=false) - type: boolean - serviceAccountName: - description: Specifies that app should be deployed authenticated via given service account, found in this namespace (optional; v0.6.0+) - type: string - syncPeriod: - description: Specifies the length of time to wait, in time + unit format, before reconciling. Always >= 30s. If value below 30s is specified, 30s will be used. (optional; v0.9.0+; default=30s) - type: string - template: - items: - properties: - cue: - properties: - inputExpression: - description: Cue expression for single path component, can be used to unify ValuesFrom into a given field (optional) - type: string - outputExpression: - description: Cue expression to output, default will export all visible fields (optional) - type: string - paths: - description: Explicit list of files/directories (optional) - items: - type: string - type: array - valuesFrom: - description: Provide values (optional) - items: - properties: - configMapRef: - properties: - name: - type: string - type: object - downwardAPI: - properties: - items: - items: - properties: - fieldPath: - description: 'Required: Selects a field of the app: only annotations, labels, uid, name and namespace are supported.' - type: string - kappControllerVersion: - description: 'Optional: Get running KappController version, defaults (empty) to retrieving the current running version.. Can be manually supplied instead.' - properties: - version: - type: string - type: object - kubernetesAPIs: - description: 'Optional: Get running KubernetesAPIs from cluster, defaults (empty) to retrieving the APIs from the cluster. Can be manually supplied instead, e.g ["group/version", "group2/version2"]' - properties: - groupVersions: - items: - type: string - type: array - type: object - kubernetesVersion: - description: 'Optional: Get running Kubernetes version from cluster, defaults (empty) to retrieving the version from the cluster. Can be manually supplied instead.' - properties: - version: - type: string - type: object - name: - type: string - type: object - type: array - type: object - path: - type: string - secretRef: - properties: - name: - type: string - type: object - type: object - type: array - type: object - helmTemplate: - description: Use helm template command to render helm chart - properties: - kubernetesAPIs: - description: 'Optional: Use kubernetes group/versions resources available in the live cluster' - properties: - groupVersions: - items: - type: string - type: array - type: object - kubernetesVersion: - description: 'Optional: Get Kubernetes version, defaults (empty) to retrieving the version from the cluster. Can be manually overridden to a value instead.' - properties: - version: - type: string - type: object - name: - description: Set name explicitly, default is App CR's name (optional; v0.13.0+) - type: string - namespace: - description: Set namespace explicitly, default is App CR's namespace (optional; v0.13.0+) - type: string - path: - description: Path to chart (optional; v0.13.0+) - type: string - valuesFrom: - description: One or more secrets, config maps, paths that provide values (optional) - items: - properties: - configMapRef: - properties: - name: - type: string - type: object - downwardAPI: - properties: - items: - items: - properties: - fieldPath: - description: 'Required: Selects a field of the app: only annotations, labels, uid, name and namespace are supported.' - type: string - kappControllerVersion: - description: 'Optional: Get running KappController version, defaults (empty) to retrieving the current running version.. Can be manually supplied instead.' - properties: - version: - type: string - type: object - kubernetesAPIs: - description: 'Optional: Get running KubernetesAPIs from cluster, defaults (empty) to retrieving the APIs from the cluster. Can be manually supplied instead, e.g ["group/version", "group2/version2"]' - properties: - groupVersions: - items: - type: string - type: array - type: object - kubernetesVersion: - description: 'Optional: Get running Kubernetes version from cluster, defaults (empty) to retrieving the version from the cluster. Can be manually supplied instead.' - properties: - version: - type: string - type: object - name: - type: string - type: object - type: array - type: object - path: - type: string - secretRef: - properties: - name: - type: string - type: object - type: object - type: array - type: object - jsonnet: - description: TODO implement jsonnet - type: object - kbld: - description: Use kbld to resolve image references to use digests - properties: - paths: - items: - type: string - type: array - type: object - kustomize: - description: TODO implement kustomize - type: object - sops: - description: Use sops to decrypt *.sops.yml files (optional; v0.11.0+) - properties: - age: - properties: - privateKeysSecretRef: - description: Secret with private armored PGP private keys (required) - properties: - name: - type: string - type: object - type: object - paths: - description: Lists paths to decrypt explicitly (optional; v0.13.0+) - items: - type: string - type: array - pgp: - description: Use PGP to decrypt files (required) - properties: - privateKeysSecretRef: - description: Secret with private armored PGP private keys (required) - properties: - name: - type: string - type: object - type: object - type: object - ytt: - description: Use ytt to template configuration - properties: - fileMarks: - description: Control metadata about input files passed to ytt (optional; v0.18.0+) see https://carvel.dev/ytt/docs/latest/file-marks/ for more details - items: - type: string - type: array - ignoreUnknownComments: - description: Ignores comments that ytt doesn't recognize (optional; default=false) - type: boolean - inline: - description: Specify additional files, including data values (optional) - properties: - paths: - additionalProperties: - type: string - description: Specifies mapping of paths to their content; not recommended for sensitive values as CR is not encrypted (optional) - type: object - pathsFrom: - description: Specifies content via secrets and config maps; data values are recommended to be placed in secrets (optional) - items: - properties: - configMapRef: - properties: - directoryPath: - description: Specifies where to place files found in secret (optional) - type: string - name: - type: string - type: object - secretRef: - properties: - directoryPath: - description: Specifies where to place files found in secret (optional) - type: string - name: - type: string - type: object - type: object - type: array - type: object - paths: - description: Lists paths to provide to ytt explicitly (optional) - items: - type: string - type: array - strict: - description: Forces strict mode https://github.com/k14s/ytt/blob/develop/docs/strict.md (optional; default=false) - type: boolean - valuesFrom: - description: Provide values via ytt's --data-values-file (optional; v0.19.0-alpha.9) - items: - properties: - configMapRef: - properties: - name: - type: string - type: object - downwardAPI: - properties: - items: - items: - properties: - fieldPath: - description: 'Required: Selects a field of the app: only annotations, labels, uid, name and namespace are supported.' - type: string - kappControllerVersion: - description: 'Optional: Get running KappController version, defaults (empty) to retrieving the current running version.. Can be manually supplied instead.' - properties: - version: - type: string - type: object - kubernetesAPIs: - description: 'Optional: Get running KubernetesAPIs from cluster, defaults (empty) to retrieving the APIs from the cluster. Can be manually supplied instead, e.g ["group/version", "group2/version2"]' - properties: - groupVersions: - items: - type: string - type: array - type: object - kubernetesVersion: - description: 'Optional: Get running Kubernetes version from cluster, defaults (empty) to retrieving the version from the cluster. Can be manually supplied instead.' - properties: - version: - type: string - type: object - name: - type: string - type: object - type: array - type: object - path: - type: string - secretRef: - properties: - name: - type: string - type: object - type: object - type: array - type: object - type: object - type: array - type: object - status: - properties: - conditions: - items: - properties: - message: - description: Human-readable message indicating details about last transition. - type: string - reason: - description: Unique, this should be a short, machine understandable string that gives the reason for condition's last transition. If it reports "ResizeStarted" that means the underlying persistent volume is being resized. - type: string - status: - type: string - type: - description: ConditionType represents reconciler state - type: string - required: - - status - - type - type: object - type: array - consecutiveReconcileFailures: - type: integer - consecutiveReconcileSuccesses: - type: integer - deploy: - properties: - error: - type: string - exitCode: - type: integer - finished: - type: boolean - kapp: - description: KappDeployStatus contains the associated AppCR deployed resources - properties: - associatedResources: - description: AssociatedResources contains the associated App label, namespaces and GKs - properties: - groupKinds: - items: - description: GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying concepts during lookup stages without having partially valid types - properties: - group: - type: string - kind: - type: string - required: - - group - - kind - type: object - type: array - label: - type: string - namespaces: - items: - type: string - type: array - type: object - type: object - startedAt: - format: date-time - type: string - stderr: - type: string - stdout: - type: string - updatedAt: - format: date-time - type: string - type: object - fetch: - properties: - error: - type: string - exitCode: - type: integer - startedAt: - format: date-time - type: string - stderr: - type: string - stdout: - type: string - updatedAt: - format: date-time - type: string - type: object - friendlyDescription: - type: string - inspect: - properties: - error: - type: string - exitCode: - type: integer - stderr: - type: string - stdout: - type: string - updatedAt: - format: date-time - type: string - type: object - managedAppName: - type: string - observedGeneration: - description: Populated based on metadata.generation when controller observes a change to the resource; if this value is out of data, other status fields do not reflect latest state - format: int64 - type: integer - template: - properties: - error: - type: string - exitCode: - type: integer - stderr: - type: string - updatedAt: - format: date-time - type: string - type: object - usefulErrorMessage: - type: string - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {}