Repository: glasskube/glasskube
Pull request #792 (2024-06-10 15:36:28)

feat!: add `ClusterPackage` CRD and change `Package` CRD scope to Namespaced
<!-- Thanks for creating this pull request 🤗 Please make sure you followed the conventional commit -->

<!-- If this pull request closes an issue, please mention the issue number below -->
Ref #803 <!-- Issue # here -->

## 📑 Description
<!-- Add a brief description of the pr -->

## ✅ Checks
- [x] My code requires changes to the documentation
- [ ] I have updated the documentation as required

## ℹ Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->


Patch:
diff --git a/PROJECT b/PROJECT
index ee91c47..a9c5a6d 100644
--- a/PROJECT
+++ b/PROJECT
@@ -37,4 +37,16 @@ resources:
   kind: PackageRepository
   path: github.com/glasskube/glasskube/api/v1alpha1
   version: v1alpha1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: glasskube.dev
+  group: packages
+  kind: ClusterPackage
+  path: github.com/glasskube/glasskube/api/v1alpha1
+  version: v1alpha1
+  webhooks:
+    validation: true
+    webhookVersion: v1
 version: "3"
diff --git a/api/v1alpha1/clusterpackage_types.go b/api/v1alpha1/clusterpackage_types.go
new file mode 100644
index 0000000..74c7124
--- /dev/null
+++ b/api/v1alpha1/clusterpackage_types.go
@@ -0,0 +1,85 @@
+/*
+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.
+*/
+
+//nolint:dupl
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
+// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
+
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+//+kubebuilder:resource:scope=Cluster,shortName=clpkg
+//+kubebuilder:printcolumn:name=Desired version,type=string,JSONPath=".spec.packageInfo.version"
+//+kubebuilder:printcolumn:name=Installed version,type=string,JSONPath=".status.version"
+
+// ClusterPackage is the Schema for the clusterpackages API
+type ClusterPackage struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   PackageSpec   `json:"spec,omitempty"`
+	Status PackageStatus `json:"status,omitempty"`
+}
+
+func (pkg *ClusterPackage) GetSpec() *PackageSpec {
+	return &pkg.Spec
+}
+
+func (in *ClusterPackage) GetStatus() *PackageStatus {
+	return &in.Status
+}
+
+func (in *ClusterPackage) AutoUpdatesEnabled() bool {
+	return autoUpdatesEnabled(in.ObjectMeta)
+}
+
+func (in *ClusterPackage) SetAutoUpdatesEnabled(enabled bool) {
+	setAutoUpdatesEnabled(&in.ObjectMeta, enabled)
+}
+
+func (pkg *ClusterPackage) InstalledAsDependency() bool {
+	return installedAsDependency(pkg.ObjectMeta)
+}
+
+func (pkg *ClusterPackage) SetInstalledAsDependency(value bool) {
+	setInstalledAsDependency(&pkg.ObjectMeta, value)
+}
+
+func (pkg *ClusterPackage) IsNamespaceScoped() bool {
+	return false
+}
+
+func (pkg *ClusterPackage) IsNil() bool {
+	return pkg == nil
+}
+
+//+kubebuilder:object:root=true
+
+// ClusterPackageList contains a list of ClusterPackage
+type ClusterPackageList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []ClusterPackage `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&ClusterPackage{}, &ClusterPackageList{})
+}
diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go
new file mode 100644
index 0000000..b48e81e
--- /dev/null
+++ b/api/v1alpha1/common.go
@@ -0,0 +1,106 @@
+package v1alpha1
+
+import (
+	"strconv"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	autoUpdateAnnotation = "packages.glasskube.dev/auto-update"
+	dependencyAnnotation = "packages.glasskube.dev/installed-as-dependency"
+)
+
+func autoUpdatesEnabled(obj metav1.ObjectMeta) bool {
+	if obj.Annotations == nil {
+		return false
+	} else if enabledStr, ok := obj.Annotations[autoUpdateAnnotation]; !ok {
+		return false
+	} else {
+		enabled, _ := strconv.ParseBool(enabledStr)
+		return enabled
+	}
+}
+
+func setAutoUpdatesEnabled(obj *metav1.ObjectMeta, enabled bool) {
+	if obj.Annotations == nil {
+		obj.Annotations = make(map[string]string)
+	}
+	if enabled {
+		obj.Annotations[autoUpdateAnnotation] = strconv.FormatBool(true)
+	} else {
+		delete(obj.Annotations, autoUpdateAnnotation)
+	}
+}
+
+func installedAsDependency(obj metav1.ObjectMeta) bool {
+	if obj.Annotations == nil {
+		return false
+	} else if enabledStr, ok := obj.Annotations[dependencyAnnotation]; !ok {
+		return false
+	} else {
+		enabled, _ := strconv.ParseBool(enabledStr)
+		return enabled
+	}
+}
+
+func setInstalledAsDependency(obj *metav1.ObjectMeta, value bool) {
+	if obj.Annotations == nil {
+		obj.Annotations = make(map[string]string)
+	}
+	if value {
+		obj.Annotations[dependencyAnnotation] = strconv.FormatBool(true)
+	} else {
+		delete(obj.Annotations, dependencyAnnotation)
+	}
+}
+
+type PackageInfoTemplate struct {
+	// Name of the package to install
+	Name string `json:"name"`
+	// Version of the package to install
+	Version string `json:"version"`
+	// RepositoryName is the name of the repository to pull the package from (optional)
+	RepositoryName string `json:"repositoryName,omitempty"`
+}
+
+type ObjectKeyValueSource struct {
+	Name      string `json:"name"`
+	Namespace string `json:"namespace,omitempty"`
+	Key       string `json:"key"`
+}
+
+type PackageValueSource struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}
+
+// +kubebuilder:validation:MinProperties:=1
+// +kubebuilder:validation:MaxProperties:=1
+type ValueReference struct {
+	ConfigMapRef *ObjectKeyValueSource `json:"configMapRef,omitempty"`
+	SecretRef    *ObjectKeyValueSource `json:"secretRef,omitempty"`
+	PackageRef   *PackageValueSource   `json:"packageRef,omitempty"`
+}
+
+// +kubebuilder:validation:MinProperties:=1
+// +kubebuilder:validation:MaxProperties:=1
+type ValueConfiguration struct {
+	Value     *string         `json:"value,omitempty"`
+	ValueFrom *ValueReference `json:"valueFrom,omitempty"`
+}
+
+// PackageSpec defines the desired state
+type PackageSpec struct {
+	PackageInfo PackageInfoTemplate           `json:"packageInfo"`
+	Values      map[string]ValueConfiguration `json:"values,omitempty"`
+}
+
+// PackageStatus defines the observed state
+type PackageStatus struct {
+	Version           string             `json:"version,omitempty"`
+	Conditions        []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	OwnedResources    []OwnedResourceRef `json:"ownedResources,omitempty"`
+	OwnedPackageInfos []OwnedResourceRef `json:"ownedPackageInfos,omitempty"`
+	OwnedPackages     []OwnedResourceRef `json:"ownedPackages,omitempty"`
+}
diff --git a/api/v1alpha1/package_manifest.go b/api/v1alpha1/package_manifest.go
index 477993d..c26a1e0 100644
--- a/api/v1alpha1/package_manifest.go
+++ b/api/v1alpha1/package_manifest.go
@@ -16,6 +16,11 @@ limitations under the License.
 
 package v1alpha1
 
+import (
+	"github.com/invopop/jsonschema"
+	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+)
+
 type HelmManifest struct {
 	// RepositoryUrl is the remote URL of the helm repository. This is the same URL you would use
 	// if you use "helm repo add ...".
@@ -63,7 +68,26 @@ type Dependency struct {
 	Version string `json:"version,omitempty"`
 }
 
+// +kubebuilder:validation:Enum=Cluster;Namespaced
+type PackageScope string
+
+const (
+	ScopeCluster    PackageScope = PackageScope(apiextv1.ClusterScoped)
+	ScopeNamespaced PackageScope = PackageScope(apiextv1.NamespaceScoped)
+)
+
+func (PackageScope) JSONSchema() *jsonschema.Schema {
+	return &jsonschema.Schema{
+		Enum: []any{
+			ScopeCluster,
+			ScopeNamespaced,
+		},
+	}
+}
+
 type PackageManifest struct {
+	// Scope is optional (default is Cluster)
+	Scope            *PackageScope      `json:"scope,omitempty"`
 	Name             string             `json:"name" jsonschema:"required"`
 	ShortDescription string             `json:"shortDescription,omitempty"`
 	LongDescription  string             `json:"longDescription,omitempty"`
diff --git a/api/v1alpha1/package_types.go b/api/v1alpha1/package_types.go
index 4e3bf3d..110816d 100644
--- a/api/v1alpha1/package_types.go
+++ b/api/v1alpha1/package_types.go
@@ -14,70 +14,19 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+//nolint:dupl
 package v1alpha1
 
 import (
-	"strconv"
-
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
 // NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
 
-type PackageInfoTemplate struct {
-	// Name of the package to install
-	Name string `json:"name"`
-	// Version of the package to install
-	Version string `json:"version"`
-	// RepositoryName is the name of the repository to pull the package from (optional)
-	RepositoryName string `json:"repositoryName,omitempty"`
-}
-
-type ObjectKeyValueSource struct {
-	Name      string `json:"name"`
-	Namespace string `json:"namespace,omitempty"`
-	Key       string `json:"key"`
-}
-
-type PackageValueSource struct {
-	Name  string `json:"name"`
-	Value string `json:"value"`
-}
-
-// +kubebuilder:validation:MinProperties:=1
-// +kubebuilder:validation:MaxProperties:=1
-type ValueReference struct {
-	ConfigMapRef *ObjectKeyValueSource `json:"configMapRef,omitempty"`
-	SecretRef    *ObjectKeyValueSource `json:"secretRef,omitempty"`
-	PackageRef   *PackageValueSource   `json:"packageRef,omitempty"`
-}
-
-// +kubebuilder:validation:MinProperties:=1
-// +kubebuilder:validation:MaxProperties:=1
-type ValueConfiguration struct {
-	Value     *string         `json:"value,omitempty"`
-	ValueFrom *ValueReference `json:"valueFrom,omitempty"`
-}
-
-// PackageSpec defines the desired state of Package
-type PackageSpec struct {
-	PackageInfo PackageInfoTemplate           `json:"packageInfo"`
-	Values      map[string]ValueConfiguration `json:"values,omitempty"`
-}
-
-// PackageStatus defines the observed state of Package
-type PackageStatus struct {
-	Version           string             `json:"version,omitempty"`
-	Conditions        []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
-	OwnedResources    []OwnedResourceRef `json:"ownedResources,omitempty"`
-	OwnedPackageInfos []OwnedResourceRef `json:"ownedPackageInfos,omitempty"`
-	OwnedPackages     []OwnedResourceRef `json:"ownedPackages,omitempty"`
-}
-
 //+kubebuilder:object:root=true
 //+kubebuilder:subresource:status
-//+kubebuilder:resource:scope=Cluster,shortName=pkg
+//+kubebuilder:resource:scope=Namespaced,shortName=pkg
 //+kubebuilder:printcolumn:name=Desired version,type=string,JSONPath=".spec.packageInfo.version"
 //+kubebuilder:printcolumn:name=Installed version,type=string,JSONPath=".status.version"
 
@@ -90,30 +39,36 @@ type Package struct {
 	Status PackageStatus `json:"status,omitempty"`
 }
 
-const (
-	autoUpdateAnnotation = "packages.glasskube.dev/auto-update"
-)
+func (pkg *Package) GetSpec() *PackageSpec {
+	return &pkg.Spec
+}
+
+func (pkg *Package) GetStatus() *PackageStatus {
+	return &pkg.Status
+}
 
 func (pkg *Package) AutoUpdatesEnabled() bool {
-	if pkg.Annotations == nil {
-		return false
-	} else if enabledStr, ok := pkg.Annotations[autoUpdateAnnotation]; !ok {
-		return false
-	} else {
-		enabled, _ := strconv.ParseBool(enabledStr)
-		return enabled
-	}
+	return autoUpdatesEnabled(pkg.ObjectMeta)
 }
 
 func (pkg *Package) SetAutoUpdatesEnabled(enabled bool) {
-	if pkg.Annotations == nil {
-		pkg.Annotations = make(map[string]string)
-	}
-	if enabled {
-		pkg.Annotations[autoUpdateAnnotation] = strconv.FormatBool(true)
-	} else {
-		delete(pkg.Annotations, autoUpdateAnnotation)
-	}
+	setAutoUpdatesEnabled(&pkg.ObjectMeta, enabled)
+}
+
+func (pkg *Package) InstalledAsDependency() bool {
+	return false
+}
+
+func (pkg *Package) SetInstalledAsDependency(value bool) {
+	panic("illegal operation: package can not be installed as dependency")
+}
+
+func (pkg *Package) IsNamespaceScoped() bool {
+	return true
+}
+
+func (pkg *Package) IsNil() bool {
+	return pkg == nil
 }
 
 //+kubebuilder:object:root=true
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 9f7d3f7..28e05cd 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -21,11 +21,70 @@ limitations under the License.
 package v1alpha1
 
 import (
-	"k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1"
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterPackage) DeepCopyInto(out *ClusterPackage) {
+	*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 ClusterPackage.
+func (in *ClusterPackage) DeepCopy() *ClusterPackage {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterPackage)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ClusterPackage) 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 *ClusterPackageList) DeepCopyInto(out *ClusterPackageList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]ClusterPackage, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterPackageList.
+func (in *ClusterPackageList) DeepCopy() *ClusterPackageList {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterPackageList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ClusterPackageList) 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 *Dependency) DeepCopyInto(out *Dependency) {
 	*out = *in
@@ -253,7 +312,7 @@ func (in *PackageInfoStatus) DeepCopyInto(out *PackageInfoStatus) {
 	}
 	if in.Conditions != nil {
 		in, out := &in.Conditions, &out.Conditions
-		*out = make([]metav1.Condition, len(*in))
+		*out = make([]v1.Condition, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -324,6 +383,11 @@ func (in *PackageList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageManifest) DeepCopyInto(out *PackageManifest) {
 	*out = *in
+	if in.Scope != nil {
+		in, out := &in.Scope, &out.Scope
+		*out = new(PackageScope)
+		**out = **in
+	}
 	if in.References != nil {
 		in, out := &in.References, &out.References
 		*out = make([]PackageReference, len(*in))
@@ -450,7 +514,7 @@ func (in *PackageRepositoryBasicAuthSpec) DeepCopyInto(out *PackageRepositoryBas
 	}
 	if in.UsernameSecretRef != nil {
 		in, out := &in.UsernameSecretRef, &out.UsernameSecretRef
-		*out = new(v1.SecretKeySelector)
+		*out = new(corev1.SecretKeySelector)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.Password != nil {
@@ -460,7 +524,7 @@ func (in *PackageRepositoryBasicAuthSpec) DeepCopyInto(out *PackageRepositoryBas
 	}
 	if in.PasswordSecretRef != nil {
 		in, out := &in.PasswordSecretRef, &out.PasswordSecretRef
-		*out = new(v1.SecretKeySelector)
+		*out = new(corev1.SecretKeySelector)
 		(*in).DeepCopyInto(*out)
 	}
 }
@@ -485,7 +549,7 @@ func (in *PackageRepositoryBearerAuthSpec) DeepCopyInto(out *PackageRepositoryBe
 	}
 	if in.TokenSecretRef != nil {
 		in, out := &in.TokenSecretRef, &out.TokenSecretRef
-		*out = new(v1.SecretKeySelector)
+		*out = new(corev1.SecretKeySelector)
 		(*in).DeepCopyInto(*out)
 	}
 }
@@ -557,7 +621,7 @@ func (in *PackageRepositoryStatus) DeepCopyInto(out *PackageRepositoryStatus) {
 	*out = *in
 	if in.Conditions != nil {
 		in, out := &in.Conditions, &out.Conditions
-		*out = make([]metav1.Condition, len(*in))
+		*out = make([]v1.Condition, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -602,7 +666,7 @@ func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
 	*out = *in
 	if in.Conditions != nil {
 		in, out := &in.Conditions, &out.Conditions
-		*out = make([]metav1.Condition, len(*in))
+		*out = make([]v1.Condition, len(*in))
 		for i := range *in {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
@@ -798,7 +862,7 @@ func (in *ValueDefinitionTarget) DeepCopyInto(out *ValueDefinitionTarget) {
 	*out = *in
 	if in.Resource != nil {
 		in, out := &in.Resource, &out.Resource
-		*out = new(v1.TypedObjectReference)
+		*out = new(corev1.TypedObjectReference)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.ChartName != nil {
diff --git a/cmd/cert-manager/main.go b/cmd/cert-manager/main.go
index eea56a1..3c4039b 100644
--- a/cmd/cert-manager/main.go
+++ b/cmd/cert-manager/main.go
@@ -16,7 +16,12 @@ import (
 )
 
 var (
-	serviceName, secretName, webhookConfigName, webhookName, namespace, certDir string
+	serviceName       = "glasskube-webhook-service"
+	secretName        = "glasskube-webhook-tls"
+	webhookConfigName = "glasskube-validating-webhook-configuration"
+	namespace         = "glasskube-system"
+	certDir           = ""
+	webhookNames      = []string{"vpackage.kb.io", "vclusterpackage.kb.io"}
 
 	log logr.Logger
 
@@ -33,17 +38,17 @@ func init() {
 	ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
 	log = ctrl.Log.WithName("cert-manager")
 
-	cmd.Flags().StringVar(&certDir, "cert-dir", "",
+	cmd.Flags().StringVar(&certDir, "cert-dir", certDir,
 		"directory for certificates (optional)")
-	cmd.Flags().StringVar(&serviceName, "service-name", "glasskube-webhook-service",
+	cmd.Flags().StringVar(&serviceName, "service-name", serviceName,
 		"name of the webhook service")
-	cmd.Flags().StringVar(&secretName, "secret-name", "glasskube-webhook-tls",
+	cmd.Flags().StringVar(&secretName, "secret-name", secretName,
 		"name of the webhook TLS secret")
-	cmd.Flags().StringVar(&webhookConfigName, "webhook-config-name", "glasskube-validating-webhook-configuration",
+	cmd.Flags().StringVar(&webhookConfigName, "webhook-config-name", webhookConfigName,
 		"name of the ValidatingWebhookConfiguration to patch")
-	cmd.Flags().StringVar(&webhookName, "webhook-name", "vpackage.kb.io",
+	cmd.Flags().StringArrayVar(&webhookNames, "webhook-name", webhookNames,
 		"name of the webhook to patch")
-	cmd.Flags().StringVar(&namespace, "namespace", "glasskube-system",
+	cmd.Flags().StringVar(&namespace, "namespace", namespace,
 		"namespace of the webhook service and TLS secret")
 }
 
@@ -95,10 +100,16 @@ func run(ctx context.Context) {
 		os.Exit(1)
 	}
 
-	webhookConfig := arv1ac.ValidatingWebhookConfiguration(webhookConfigName).WithWebhooks(
-		arv1ac.ValidatingWebhook().WithName(webhookName).
-			WithClientConfig(arv1ac.WebhookClientConfig().WithCABundle(caEnc.Cert...)),
-	)
+	webhookConfig := arv1ac.ValidatingWebhookConfiguration(webhookConfigName)
+	for _, name := range webhookNames {
+		webhookConfig.WithWebhooks(
+			arv1ac.ValidatingWebhook().
+				WithName(name).
+				WithClientConfig(arv1ac.WebhookClientConfig().
+					WithCABundle(caEnc.Cert...),
+				),
+		)
+	}
 
 	if _, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().
 		Apply(ctx, webhookConfig, applyOptions); err != nil {
diff --git a/cmd/glasskube/cmd/auto-update.go b/cmd/glasskube/cmd/auto-update.go
index 162ef73..f72451d 100644
--- a/cmd/glasskube/cmd/auto-update.go
+++ b/cmd/glasskube/cmd/auto-update.go
@@ -38,14 +38,14 @@ func runAutoUpdateEnableOrDisable(enabled bool, confirmMsg, successMsg string) f
 	return func(cmd *cobra.Command, args []string) {
 		ctx := cmd.Context()
 		client := cliutils.PackageClient(ctx)
-		var pkgs []v1alpha1.Package
+		var pkgs []v1alpha1.ClusterPackage
 		if autoUpdateEnabledDisabledOptions.All {
 			if len(args) > 0 {
 				fmt.Fprintf(os.Stderr, "Too many arguments: %v\n", args)
 				cliutils.ExitWithError()
 			}
-			var pkgList v1alpha1.PackageList
-			if err := client.Packages().GetAll(ctx, &pkgList); err != nil {
+			var pkgList v1alpha1.ClusterPackageList
+			if err := client.ClusterPackages().GetAll(ctx, &pkgList); err != nil {
 				fmt.Fprintf(os.Stderr, "Could not list packages: %v", err)
 				cliutils.ExitWithError()
 			}
@@ -58,10 +58,10 @@ func runAutoUpdateEnableOrDisable(enabled bool, confirmMsg, successMsg string) f
 				fmt.Fprintln(os.Stderr, "Please specify at least one package")
 				cliutils.ExitWithError()
 			}
-			pkgs = make([]v1alpha1.Package, len(args))
+			pkgs = make([]v1alpha1.ClusterPackage, len(args))
 			for i, name := range args {
-				var pkg v1alpha1.Package
-				if err := client.Packages().Get(ctx, name, &pkg); err != nil {
+				var pkg v1alpha1.ClusterPackage
+				if err := client.ClusterPackages().Get(ctx, name, &pkg); err != nil {
 					fmt.Fprintf(os.Stderr, "Could not get package %v: %v", name, err)
 					cliutils.ExitWithError()
 				}
@@ -78,7 +78,7 @@ func runAutoUpdateEnableOrDisable(enabled bool, confirmMsg, successMsg string) f
 		for _, pkg := range pkgs {
 			if pkg.AutoUpdatesEnabled() != enabled {
 				pkg.SetAutoUpdatesEnabled(enabled)
-				multierr.AppendInto(&err, client.Packages().Update(ctx, &pkg))
+				multierr.AppendInto(&err, client.ClusterPackages().Update(ctx, &pkg))
 			}
 		}
 		if err != nil {
@@ -107,8 +107,8 @@ func runAutoUpdate(cmd *cobra.Command, args []string) {
 	updater := update.NewUpdater(ctx).
 		WithStatusWriter(statuswriter.Stderr())
 
-	var pkgs v1alpha1.PackageList
-	if err := client.Packages().GetAll(ctx, &pkgs); err != nil {
+	var pkgs v1alpha1.ClusterPackageList
+	if err := client.ClusterPackages().GetAll(ctx, &pkgs); err != nil {
 		panic(err)
 	}
 
diff --git a/cmd/glasskube/cmd/configure.go b/cmd/glasskube/cmd/configure.go
index 99cda0c..35fd169 100644
--- a/cmd/glasskube/cmd/configure.go
+++ b/cmd/glasskube/cmd/configure.go
@@ -19,8 +19,11 @@ import (
 var configureCmdOptions = struct {
 	flags.ValuesOptions
 	OutputOptions
+	NamespaceOptions
+	KindOptions
 }{
 	ValuesOptions: flags.NewOptions(flags.WithKeepOldValuesFlag),
+	KindOptions:   DefaultKindOptions(),
 }
 
 var configureCmd = &cobra.Command{
@@ -37,36 +40,37 @@ func runConfigure(cmd *cobra.Command, args []string) {
 	ctx := cmd.Context()
 	pkgClient := cliutils.PackageClient(ctx)
 	valueResolver := cliutils.ValueResolver(ctx)
-	pkgName := args[0]
-	var pkg v1alpha1.Package
 
-	if err := pkgClient.Packages().Get(ctx, pkgName, &pkg); err != nil {
-		fmt.Fprintf(os.Stderr, "❌ error getting package: %v\n", err)
+	name := args[0]
+	pkg, err :=
+		getPackageOrClusterPackage(ctx, name, configureCmdOptions.KindOptions, configureCmdOptions.NamespaceOptions)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "❌ could not get resource: %v\n", err)
 		cliutils.ExitWithError()
 	}
 
 	if configureCmdOptions.IsValuesSet() {
-		if values, err := configureCmdOptions.ParseValues(pkg.Spec.Values); err != nil {
+		if values, err := configureCmdOptions.ParseValues(pkg.GetSpec().Values); err != nil {
 			fmt.Fprintf(os.Stderr, "❌ invalid values in command line flags: %v\n", err)
 			cliutils.ExitWithError()
 		} else {
-			pkg.Spec.Values = values
+			pkg.GetSpec().Values = values
 		}
 	} else {
 		if pkgManifest, err := manifest.GetInstalledManifestForPackage(ctx, pkg); err != nil {
 			fmt.Fprintf(os.Stderr, "❌ error getting installed manifest: %v\n", err)
 			cliutils.ExitWithError()
-		} else if values, err := cli.Configure(*pkgManifest, pkg.Spec.Values); err != nil {
+		} else if values, err := cli.Configure(*pkgManifest, pkg.GetSpec().Values); err != nil {
 			fmt.Fprintf(os.Stderr, "❌ error during configure: %v\n", err)
 			cliutils.ExitWithError()
 		} else {
-			pkg.Spec.Values = values
+			pkg.GetSpec().Values = values
 		}
 	}
 
 	fmt.Fprintln(os.Stderr, bold("Configuration:"))
-	printValueConfigurations(os.Stderr, pkg.Spec.Values)
-	if _, err := valueResolver.Resolve(ctx, pkg.Spec.Values); err != nil {
+	printValueConfigurations(os.Stderr, pkg.GetSpec().Values)
+	if _, err := valueResolver.Resolve(ctx, pkg.GetSpec().Values); err != nil {
 		fmt.Fprintf(os.Stderr, "⚠️  Some values can not be resolved: %v\n", err)
 	}
 
@@ -74,20 +78,36 @@ func runConfigure(cmd *cobra.Command, args []string) {
 		cancel()
 	}
 
-	if err := pkgClient.Packages().Get(ctx, pkgName, &pkg); err != nil {
-		// Don't exit, we can still try to call update ...
-		fmt.Fprintf(os.Stderr, "⚠️  error fetching package: %v\n", err)
-	}
+	switch pkg := pkg.(type) {
+	case *v1alpha1.ClusterPackage:
+		if err := pkgClient.ClusterPackages().Get(ctx, pkg.Name, pkg); err != nil {
+			// Don't exit, we can still try to call update ...
+			fmt.Fprintf(os.Stderr, "⚠️  error fetching package: %v\n", err)
+		}
+
+		if err := pkgClient.ClusterPackages().Update(ctx, pkg); err != nil {
+			fmt.Fprintf(os.Stderr, "❌ error updating package: %v\n", err)
+			cliutils.ExitWithError()
+		}
+	case *v1alpha1.Package:
+		if err := pkgClient.Packages(pkg.Namespace).Get(ctx, pkg.Name, pkg); err != nil {
+			// Don't exit, we can still try to call update ...
+			fmt.Fprintf(os.Stderr, "⚠️  error fetching package: %v\n", err)
+		}
 
-	if err := pkgClient.Packages().Update(ctx, &pkg); err != nil {
-		fmt.Fprintf(os.Stderr, "❌ error updating package: %v\n", err)
+		if err := pkgClient.Packages(pkg.Namespace).Update(ctx, pkg); err != nil {
+			fmt.Fprintf(os.Stderr, "❌ error updating package: %v\n", err)
+			cliutils.ExitWithError()
+		}
+	default:
+		fmt.Fprintln(os.Stderr, "❌ invalid state: pkg must be either Package or ClusterPackage")
 		cliutils.ExitWithError()
-	} else {
-		fmt.Fprintln(os.Stderr, "✅ configuration changed")
 	}
 
+	fmt.Fprintln(os.Stderr, "✅ configuration changed")
+
 	if configureCmdOptions.Output != "" {
-		if gvks, _, err := scheme.Scheme.ObjectKinds(&pkg); err == nil && len(gvks) == 1 {
+		if gvks, _, err := scheme.Scheme.ObjectKinds(pkg); err == nil && len(gvks) == 1 {
 			pkg.SetGroupVersionKind(gvks[0])
 		}
 		var output []byte
@@ -112,5 +132,8 @@ func runConfigure(cmd *cobra.Command, args []string) {
 func init() {
 	configureCmdOptions.ValuesOptions.AddFlagsToCommand(configureCmd)
 	configureCmdOptions.OutputOptions.AddFlagsToCommand(configureCmd)
+	// TODO: Enable these flags to support namespaced packages
+	// configureCmdOptions.NamespaceOptions.AddFlagsToCommand(configureCmd)
+	// configureCmdOptions.KindOptions.AddFlagsToCommand(configureCmd)
 	RootCmd.AddCommand(configureCmd)
 }
diff --git a/cmd/glasskube/cmd/describe.go b/cmd/glasskube/cmd/describe.go
index 0a76c33..803d6df 100644
--- a/cmd/glasskube/cmd/describe.go
+++ b/cmd/glasskube/cmd/describe.go
@@ -154,7 +154,7 @@ func printDependencies(manifest *v1alpha1.PackageManifest) {
 	}
 }
 
-func printRepositories(pkg *v1alpha1.Package, repos []v1alpha1.PackageRepository) {
+func printRepositories(pkg *v1alpha1.ClusterPackage, repos []v1alpha1.PackageRepository) {
 	for _, repo := range repos {
 		fmt.Fprintf(os.Stderr, " * %v", repo.Name)
 		if isInstalledFrom(pkg, repo) {
@@ -165,7 +165,7 @@ func printRepositories(pkg *v1alpha1.Package, repos []v1alpha1.PackageRepository
 	}
 }
 
-func repositoriesAsMap(pkg *v1alpha1.Package, repos []v1alpha1.PackageRepository) []map[string]any {
+func repositoriesAsMap(pkg *v1alpha1.ClusterPackage, repos []v1alpha1.PackageRepository) []map[string]any {
 	repositories := make([]map[string]any, 0, len(repos))
 	for _, repo := range repos {
 		repositories = append(repositories, map[string]any{
@@ -176,17 +176,17 @@ func repositoriesAsMap(pkg *v1alpha1.Package, repos []v1alpha1.PackageRepository
 	return repositories
 }
 
-func isInstalledFrom(pkg *v1alpha1.Package, repo v1alpha1.PackageRepository) bool {
+func isInstalledFrom(pkg *v1alpha1.ClusterPackage, repo v1alpha1.PackageRepository) bool {
 	return pkg != nil &&
 		(repo.Name == pkg.Spec.PackageInfo.RepositoryName ||
 			(len(pkg.Spec.PackageInfo.RepositoryName) == 0 && repo.IsDefaultRepository()))
 }
 
-func printReferences(ctx context.Context, pkg *v1alpha1.Package, manifest *v1alpha1.PackageManifest) {
+func printReferences(ctx context.Context, pkg *v1alpha1.ClusterPackage, manifest *v1alpha1.PackageManifest) {
 	repo := cliutils.RepositoryClientset(ctx)
 	var repoClient repoclient.RepoClient
 	if pkg != nil {
-		repoClient = repo.ForPackage(*pkg)
+		repoClient = repo.ForPackage(pkg)
 		if url, err := repoClient.GetPackageManifestURL(manifest.Name, pkg.Spec.PackageInfo.Version); err != nil {
 			fmt.Fprintf(os.Stderr, "❌ Could not get package manifest url: %v\n", err)
 		} else {
@@ -200,7 +200,7 @@ func printReferences(ctx context.Context, pkg *v1alpha1.Package, manifest *v1alp
 
 func referencesAsMap(
 	ctx context.Context,
-	pkg *v1alpha1.Package,
+	pkg *v1alpha1.ClusterPackage,
 	manifest *v1alpha1.PackageManifest,
 ) []map[string]string {
 	references := []map[string]string{}
@@ -212,7 +212,7 @@ func referencesAsMap(
 	}
 	if pkg != nil {
 		repo := cliutils.RepositoryClientset(ctx)
-		repoClient := repo.ForPackage(*pkg)
+		repoClient := repo.ForPackage(pkg)
 		if url, err := repoClient.GetPackageManifestURL(manifest.Name, pkg.Spec.PackageInfo.Version); err == nil {
 			reference := make(map[string]string)
 			reference["label"] = "Glasskube Package Manifest"
@@ -245,7 +245,7 @@ func printMarkdown(w io.Writer, text string) {
 	}
 }
 
-func status(pkg *v1alpha1.Package) string {
+func status(pkg *v1alpha1.ClusterPackage) string {
 	pkgStatus := client.GetStatusOrPending(pkg)
 	if pkgStatus != nil {
 		switch pkgStatus.Status {
@@ -261,7 +261,7 @@ func status(pkg *v1alpha1.Package) string {
 	}
 }
 
-func version(pkg *v1alpha1.Package, latestVersion string) string {
+func version(pkg *v1alpha1.ClusterPackage, latestVersion string) string {
 	if pkg != nil {
 		var parts []string
 		if len(pkg.Status.Version) > 0 {
@@ -292,7 +292,7 @@ func nameAndDescription(manifest *v1alpha1.PackageManifest) string {
 
 func createOutputStructure(
 	ctx context.Context,
-	pkg *v1alpha1.Package,
+	pkg *v1alpha1.ClusterPackage,
 	manifest *v1alpha1.PackageManifest,
 	latestVersion string,
 	repos []v1alpha1.PackageRepository,
@@ -320,7 +320,7 @@ func createOutputStructure(
 }
 
 func printJSON(ctx context.Context,
-	pkg *v1alpha1.Package,
+	pkg *v1alpha1.ClusterPackage,
 	manifest *v1alpha1.PackageManifest,
 	latestVersion string,
 	repos []v1alpha1.PackageRepository) {
@@ -334,7 +334,7 @@ func printJSON(ctx context.Context,
 }
 
 func printYAML(ctx context.Context,
-	pkg *v1alpha1.Package,
+	pkg *v1alpha1.ClusterPackage,
 	manifest *v1alpha1.PackageManifest,
 	latestVersion string,
 	repos []v1alpha1.PackageRepository) {
diff --git a/cmd/glasskube/cmd/install.go b/cmd/glasskube/cmd/install.go
index 239178f..eb9403d 100644
--- a/cmd/glasskube/cmd/install.go
+++ b/cmd/glasskube/cmd/install.go
@@ -65,7 +65,7 @@ var installCmd = &cobra.Command{
 
 		bold := color.New(color.Bold).SprintFunc()
 		packageName := args[0]
-		pkgBuilder := client.PackageBuilder(packageName)
+		pkgBuilder := client.ClusterPackageBuilder(packageName)
 		var repoClient repoclient.RepoClient
 
 		if len(installCmdOptions.Repository) > 0 {
@@ -228,7 +228,7 @@ var installCmd = &cobra.Command{
 	},
 }
 
-func formatOutput(pkg *v1alpha1.Package, format OutputFormat) (string, error) {
+func formatOutput(pkg *v1alpha1.ClusterPackage, format OutputFormat) (string, error) {
 	if gvks, _, err := scheme.Scheme.ObjectKinds(pkg); err == nil && len(gvks) == 1 {
 		pkg.SetGroupVersionKind(gvks[0])
 	}
diff --git a/cmd/glasskube/cmd/kind_options.go b/cmd/glasskube/cmd/kind_options.go
new file mode 100644
index 0000000..ebaeb63
--- /dev/null
+++ b/cmd/glasskube/cmd/kind_options.go
@@ -0,0 +1,116 @@
+package cmd
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/cliutils"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
+	"github.com/spf13/cobra"
+	"go.uber.org/multierr"
+	"k8s.io/apimachinery/pkg/api/errors"
+)
+
+type ResourceKind string
+
+const (
+	KindUnspecified    ResourceKind = ""
+	KindPackage        ResourceKind = "package"
+	KindClusterPackage ResourceKind = "clusterpackage"
+)
+
+// Set implements pflag.Value.
+func (kind *ResourceKind) Set(value string) error {
+	switch ResourceKind(value) {
+	case KindPackage, KindClusterPackage:
+		*kind = ResourceKind(value)
+		return nil
+	default:
+		return fmt.Errorf("invalid kind: %s", value)
+	}
+}
+
+// String implements pflag.Value.
+func (kind *ResourceKind) String() string {
+	return string(*kind)
+}
+
+// Type implements pflag.Value.
+func (r *ResourceKind) Type() string {
+	return fmt.Sprintf("[%v|%v]", KindPackage, KindClusterPackage)
+}
+
+type KindOptions struct {
+	Kind ResourceKind
+}
+
+func (opts *KindOptions) AddFlagsToCommand(cmd *cobra.Command) {
+	cmd.Flags().Var(&opts.Kind, "kind", "specify the kind of the resource")
+}
+
+func DefaultKindOptions() KindOptions {
+	return KindOptions{
+		// TODO: Change to KindUnspecified to support namespaced packages
+		Kind: KindClusterPackage,
+	}
+}
+
+func getPackageOrClusterPackage(
+	ctx context.Context, name string, kindOpts KindOptions, nsOpts NamespaceOptions) (ctrlpkg.Package, error) {
+
+	pkgClient := cliutils.PackageClient(ctx)
+	var pkg v1alpha1.Package
+	var cpkg v1alpha1.ClusterPackage
+	// store errors separate because multierr is not threadsafe
+	var errp, errcp error
+	var pkgTried, cpkgTried bool
+
+	var wg sync.WaitGroup
+	if kindOpts.Kind == KindUnspecified || kindOpts.Kind == KindPackage {
+		wg.Add(1)
+		pkgTried = true
+		go func() {
+			defer wg.Done()
+			namespace := nsOpts.GetActualNamespace(ctx)
+			errp = pkgClient.Packages(namespace).Get(ctx, name, &pkg)
+		}()
+	}
+	if nsOpts.Namespace == "" &&
+		(kindOpts.Kind == KindUnspecified || kindOpts.Kind == KindClusterPackage) {
+		// If a namespace was specified explicitly via a flag, we don't have to try to get the ClusterPackage.
+		wg.Add(1)
+		cpkgTried = true
+		go func() {
+			defer wg.Done()
+			errcp = pkgClient.ClusterPackages().Get(ctx, name, &cpkg)
+		}()
+	}
+	wg.Wait()
+
+	// check errors other than "not found"
+	var err error
+	if errp != nil && !errors.IsNotFound(errp) {
+		multierr.AppendInto(&err, errp)
+	}
+	if errcp != nil && !errors.IsNotFound(errcp) {
+		multierr.AppendInto(&err, errcp)
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	// from here on, err == nil implies "not found"
+	pNotFound := !pkgTried || errp != nil
+	cpNotFound := !cpkgTried || errcp != nil
+	if pNotFound && cpNotFound {
+		return nil, fmt.Errorf("no Package or ClusterPackage found with name %v", name)
+	} else if !pNotFound && !cpNotFound {
+		return nil, fmt.Errorf("both Package and ClusterPackage found with name %v. Please specify the kind explicitly", name)
+	} else if !pNotFound && cpNotFound {
+		return &pkg, nil
+	} else {
+		return &cpkg, nil
+	}
+}
diff --git a/cmd/glasskube/cmd/namespace_options.go b/cmd/glasskube/cmd/namespace_options.go
new file mode 100644
index 0000000..2ebec1b
--- /dev/null
+++ b/cmd/glasskube/cmd/namespace_options.go
@@ -0,0 +1,29 @@
+package cmd
+
+import (
+	"context"
+
+	"github.com/glasskube/glasskube/internal/clicontext"
+	"github.com/spf13/cobra"
+)
+
+type NamespaceOptions struct {
+	Namespace string
+}
+
+func (opt *NamespaceOptions) AddFlagsToCommand(cmd *cobra.Command) {
+	cmd.Flags().StringVarP(&opt.Namespace, "namespace", "n", opt.Namespace, "namespace for resources")
+}
+
+func (opt *NamespaceOptions) GetActualNamespace(ctx context.Context) string {
+	if configureCmdOptions.Namespace != "" {
+		return configureCmdOptions.Namespace
+	} else {
+		rawConfig := clicontext.RawConfigFromContext(ctx)
+		if current, ok := rawConfig.Contexts[rawConfig.CurrentContext]; ok && current.Namespace != "" {
+			return current.Namespace
+		} else {
+			return "default"
+		}
+	}
+}
diff --git a/cmd/glasskube/cmd/open.go b/cmd/glasskube/cmd/open.go
index 8af412e..36f2d96 100644
--- a/cmd/glasskube/cmd/open.go
+++ b/cmd/glasskube/cmd/open.go
@@ -11,8 +11,16 @@ import (
 	"github.com/spf13/cobra"
 )
 
+type OpenCmdOptions struct {
+	NamespaceOptions
+	KindOptions
+	Port int32
+}
+
 var (
-	port int32
+	openCmdOptions = OpenCmdOptions{
+		KindOptions: DefaultKindOptions(),
+	}
 )
 
 var openCmd = &cobra.Command{
@@ -23,13 +31,20 @@ If the package manifest has more than one entrypoint, specify the name of the en
 	Args:   cobra.RangeArgs(1, 2),
 	PreRun: cliutils.SetupClientContext(true, &rootCmdOptions.SkipUpdateCheck),
 	Run: func(cmd *cobra.Command, args []string) {
+		ctx := cmd.Context()
 		pkgName := args[0]
-		var entrypointName string
+		entrypointName := ""
 		if len(args) == 2 {
 			entrypointName = args[1]
 		}
 
-		result, err := open.NewOpener().Open(cmd.Context(), pkgName, entrypointName, port)
+		pkg, err := getPackageOrClusterPackage(ctx, pkgName, openCmdOptions.KindOptions, openCmdOptions.NamespaceOptions)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "❌ Could not get resource %v: %v\n", pkgName, err)
+			cliutils.ExitWithError()
+		}
+
+		result, err := open.NewOpener().Open(ctx, pkg, entrypointName, openCmdOptions.Port)
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "❌ Could not open package %v: %v\n", pkgName, err)
 			cliutils.ExitWithError()
@@ -74,6 +89,9 @@ If the package manifest has more than one entrypoint, specify the name of the en
 }
 
 func init() {
-	openCmd.Flags().Int32Var(&port, "port", 0, "Custom port for opening the package")
+	// TODO: Enable these flags to support namespaced packages
+	// openCmdOptions.KindOptions.AddFlagsToCommand(openCmd)
+	// openCmdOptions.NamespaceOptions.AddFlagsToCommand(openCmd)
+	openCmd.Flags().Int32Var(&openCmdOptions.Port, "port", openCmdOptions.Port, "custom port for opening the package")
 	RootCmd.AddCommand(openCmd)
 }
diff --git a/cmd/glasskube/cmd/uninstall.go b/cmd/glasskube/cmd/uninstall.go
index 9e3d62a..7fd9d0c 100644
--- a/cmd/glasskube/cmd/uninstall.go
+++ b/cmd/glasskube/cmd/uninstall.go
@@ -5,12 +5,13 @@ import (
 	"os"
 
 	"github.com/fatih/color"
+	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/clicontext"
 	"github.com/glasskube/glasskube/internal/cliutils"
-	pkgClient "github.com/glasskube/glasskube/pkg/client"
 	"github.com/glasskube/glasskube/pkg/statuswriter"
 	"github.com/glasskube/glasskube/pkg/uninstall"
 	"github.com/spf13/cobra"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 var uninstallCmdOptions = struct {
@@ -55,15 +56,15 @@ var uninstallCmd = &cobra.Command{
 			uninstaller.WithStatusWriter(statuswriter.Spinner())
 		}
 
-		pkg := pkgClient.NewPackage(pkgName, "")
+		pkg := v1alpha1.ClusterPackage{ObjectMeta: metav1.ObjectMeta{Name: pkgName}}
 		if uninstallCmdOptions.NoWait {
-			if err := uninstaller.Uninstall(ctx, pkg); err != nil {
+			if err := uninstaller.Uninstall(ctx, &pkg); err != nil {
 				fmt.Fprintf(os.Stderr, "\n❌ An error occurred during uninstallation:\n\n%v\n", err)
 				cliutils.ExitWithError()
 			}
 			fmt.Fprintln(os.Stderr, "Uninstallation started in background")
 		} else {
-			if err := uninstaller.UninstallBlocking(ctx, pkg); err != nil {
+			if err := uninstaller.UninstallBlocking(ctx, &pkg); err != nil {
 				fmt.Fprintf(os.Stderr, "\n❌ An error occurred during uninstallation:\n\n%v\n", err)
 				cliutils.ExitWithError()
 			}
diff --git a/cmd/glasskube/cmd/update.go b/cmd/glasskube/cmd/update.go
index 108f4bd..3c90299 100644
--- a/cmd/glasskube/cmd/update.go
+++ b/cmd/glasskube/cmd/update.go
@@ -103,7 +103,7 @@ func printTransaction(tx update.UpdateTransaction) {
 	_ = w.Flush()
 }
 
-func handleOutput(pkgs []v1alpha1.Package) {
+func handleOutput(pkgs []v1alpha1.ClusterPackage) {
 	if updateCmdOptions.Output == "" {
 		return
 	}
@@ -165,8 +165,8 @@ func completeInstalledPackageNames(
 		dir &= cobra.ShellCompDirectiveError
 		return
 	}
-	var packageList v1alpha1.PackageList
-	if err := client.Packages().GetAll(cmd.Context(), &packageList); err != nil {
+	var packageList v1alpha1.ClusterPackageList
+	if err := client.ClusterPackages().GetAll(cmd.Context(), &packageList); err != nil {
 		dir &= cobra.ShellCompDirectiveError
 		return
 	}
@@ -202,13 +202,13 @@ func completeUpgradablePackageVersions(
 	client := cliutils.PackageClient(ctx)
 	repoClient := cliutils.RepositoryClientset(ctx)
 
-	var pkg v1alpha1.Package
-	if err := client.Packages().Get(cmd.Context(), packageName, &pkg); err != nil {
+	var pkg v1alpha1.ClusterPackage
+	if err := client.ClusterPackages().Get(cmd.Context(), packageName, &pkg); err != nil {
 		dir &= cobra.ShellCompDirectiveError
 		return nil, dir
 	}
 	var packageIndex repo.PackageIndex
-	if err := repoClient.ForPackage(pkg).FetchPackageIndex(packageName, &packageIndex); err != nil {
+	if err := repoClient.ForPackage(&pkg).FetchPackageIndex(packageName, &packageIndex); err != nil {
 		return nil, cobra.ShellCompDirectiveError
 	}
 	versions := make([]string, 0, len(packageIndex.Versions))
diff --git a/cmd/package-operator/main.go b/cmd/package-operator/main.go
index be9ae05..46f44ee 100644
--- a/cmd/package-operator/main.go
+++ b/cmd/package-operator/main.go
@@ -21,19 +21,15 @@ import (
 	"flag"
 	"os"
 
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+
 	ctrladapter "github.com/glasskube/glasskube/internal/adapter/controllerruntime"
 	"github.com/glasskube/glasskube/internal/dependency"
 	repoclient "github.com/glasskube/glasskube/internal/repo/client"
 	"github.com/glasskube/glasskube/internal/telemetry"
-	"sigs.k8s.io/controller-runtime/pkg/manager"
 
 	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
 	// to ensure that exec-entrypoint and run can make use of them.
-	packagesv1alpha1 "github.com/glasskube/glasskube/api/v1alpha1"
-	"github.com/glasskube/glasskube/internal/controller"
-	"github.com/glasskube/glasskube/internal/manifest/helm/flux"
-	"github.com/glasskube/glasskube/internal/manifest/plain"
-	"github.com/glasskube/glasskube/internal/webhook"
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -42,6 +38,12 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/healthz"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+
+	packagesv1alpha1 "github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller"
+	"github.com/glasskube/glasskube/internal/manifest/helm/flux"
+	"github.com/glasskube/glasskube/internal/manifest/plain"
+	"github.com/glasskube/glasskube/internal/webhook"
 	//+kubebuilder:scaffold:imports
 )
 
@@ -107,7 +109,7 @@ func main() {
 	)
 
 	telemetry.InitWithManager(mgr)
-	if err = (&controller.PackageReconciler{
+	commonReconciler := controller.PackageReconcilerCommon{
 		Client:            mgr.GetClient(),
 		EventRecorder:     mgr.GetEventRecorderFor("package-controller"),
 		Scheme:            mgr.GetScheme(),
@@ -115,10 +117,19 @@ func main() {
 		ManifestAdapter:   plain.NewAdapter(),
 		RepoClientset:     repoClient,
 		DependencyManager: dependencyManager,
+	}
+	if err = (&controller.PackageReconciler{
+		PackageReconcilerCommon: commonReconciler,
 	}).SetupWithManager(mgr); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "Package")
 		os.Exit(1)
 	}
+	if err = (&controller.ClusterPackageReconciler{
+		PackageReconcilerCommon: commonReconciler,
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "ClusterPackage")
+		os.Exit(1)
+	}
 	if err = (&controller.PackageInfoReconciler{
 		Client:        mgr.GetClient(),
 		EventRecorder: mgr.GetEventRecorderFor("packageinfo-controller"),
diff --git a/config/crd/bases/packages.glasskube.dev_clusterpackages.yaml b/config/crd/bases/packages.glasskube.dev_clusterpackages.yaml
new file mode 100644
index 0000000..599ce7c
--- /dev/null
+++ b/config/crd/bases/packages.glasskube.dev_clusterpackages.yaml
@@ -0,0 +1,263 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.14.0
+  name: clusterpackages.packages.glasskube.dev
+spec:
+  group: packages.glasskube.dev
+  names:
+    kind: ClusterPackage
+    listKind: ClusterPackageList
+    plural: clusterpackages
+    shortNames:
+    - clpkg
+    singular: clusterpackage
+  scope: Cluster
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.packageInfo.version
+      name: Desired version
+      type: string
+    - jsonPath: .status.version
+      name: Installed version
+      type: string
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: ClusterPackage is the Schema for the clusterpackages API
+        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:
+            description: PackageSpec defines the desired state
+            properties:
+              packageInfo:
+                properties:
+                  name:
+                    description: Name of the package to install
+                    type: string
+                  repositoryName:
+                    description: RepositoryName is the name of the repository to pull
+                      the package from (optional)
+                    type: string
+                  version:
+                    description: Version of the package to install
+                    type: string
+                required:
+                - name
+                - version
+                type: object
+              values:
+                additionalProperties:
+                  maxProperties: 1
+                  minProperties: 1
+                  properties:
+                    value:
+                      type: string
+                    valueFrom:
+                      maxProperties: 1
+                      minProperties: 1
+                      properties:
+                        configMapRef:
+                          properties:
+                            key:
+                              type: string
+                            name:
+                              type: string
+                            namespace:
+                              type: string
+                          required:
+                          - key
+                          - name
+                          type: object
+                        packageRef:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          required:
+                          - name
+                          - value
+                          type: object
+                        secretRef:
+                          properties:
+                            key:
+                              type: string
+                            name:
+                              type: string
+                            namespace:
+                              type: string
+                          required:
+                          - key
+                          - name
+                          type: object
+                      type: object
+                  type: object
+                type: object
+            required:
+            - packageInfo
+            type: object
+          status:
+            description: PackageStatus defines the observed state
+            properties:
+              conditions:
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource.\n---\nThis struct is intended for
+                    direct use as an array at the field path .status.conditions.  For
+                    example,\n\n\n\ttype FooStatus struct{\n\t    // Represents the
+                    observations of a foo's current state.\n\t    // Known .status.conditions.type
+                    are: \"Available\", \"Progressing\", and \"Degraded\"\n\t    //
+                    +patchMergeKey=type\n\t    // +patchStrategy=merge\n\t    // +listType=map\n\t
+                    \   // +listMapKey=type\n\t    Conditions []metav1.Condition `json:\"conditions,omitempty\"
+                    patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+                    \   // other fields\n\t}"
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: |-
+                        type of condition in CamelCase or in foo.example.com/CamelCase.
+                        ---
+                        Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+                        useful (see .node.status.conditions), the ability to deconflict is important.
+                        The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+              ownedPackageInfos:
+                items:
+                  properties:
+                    group:
+                      type: string
+                    kind:
+                      type: string
+                    markedForDeletion:
+                      type: boolean
+                    name:
+                      type: string
+                    namespace:
+                      type: string
+                    version:
+                      type: string
+                  required:
+                  - group
+                  - kind
+                  - name
+                  - version
+                  type: object
+                type: array
+              ownedPackages:
+                items:
+                  properties:
+                    group:
+                      type: string
+                    kind:
+                      type: string
+                    markedForDeletion:
+                      type: boolean
+                    name:
+                      type: string
+                    namespace:
+                      type: string
+                    version:
+                      type: string
+                  required:
+                  - group
+                  - kind
+                  - name
+                  - version
+                  type: object
+                type: array
+              ownedResources:
+                items:
+                  properties:
+                    group:
+                      type: string
+                    kind:
+                      type: string
+                    markedForDeletion:
+                      type: boolean
+                    name:
+                      type: string
+                    namespace:
+                      type: string
+                    version:
+                      type: string
+                  required:
+                  - group
+                  - kind
+                  - name
+                  - version
+                  type: object
+                type: array
+              version:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/config/crd/bases/packages.glasskube.dev_packageinfos.yaml b/config/crd/bases/packages.glasskube.dev_packageinfos.yaml
index b1c8eda..9e36ad3 100644
--- a/config/crd/bases/packages.glasskube.dev_packageinfos.yaml
+++ b/config/crd/bases/packages.glasskube.dev_packageinfos.yaml
@@ -241,6 +241,12 @@ spec:
                       - url
                       type: object
                     type: array
+                  scope:
+                    description: Scope is optional (default is Cluster)
+                    enum:
+                    - Cluster
+                    - Namespaced
+                    type: string
                   shortDescription:
                     type: string
                   valueDefinitions:
diff --git a/config/crd/bases/packages.glasskube.dev_packages.yaml b/config/crd/bases/packages.glasskube.dev_packages.yaml
index 029fd78..a2d3911 100644
--- a/config/crd/bases/packages.glasskube.dev_packages.yaml
+++ b/config/crd/bases/packages.glasskube.dev_packages.yaml
@@ -14,7 +14,7 @@ spec:
     shortNames:
     - pkg
     singular: package
-  scope: Cluster
+  scope: Namespaced
   versions:
   - additionalPrinterColumns:
     - jsonPath: .spec.packageInfo.version
@@ -46,7 +46,7 @@ spec:
           metadata:
             type: object
           spec:
-            description: PackageSpec defines the desired state of Package
+            description: PackageSpec defines the desired state
             properties:
               packageInfo:
                 properties:
@@ -116,7 +116,7 @@ spec:
             - packageInfo
             type: object
           status:
-            description: PackageStatus defines the observed state of Package
+            description: PackageStatus defines the observed state
             properties:
               conditions:
                 items:
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 4ecd20b..140a4f1 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -2,15 +2,17 @@
 # since it depends on service name and namespace that are out of this kustomize package.
 # It should be run by config/default
 resources:
-- bases/packages.glasskube.dev_packages.yaml
-- bases/packages.glasskube.dev_packageinfos.yaml
-- bases/packages.glasskube.dev_packagerepositories.yaml
+  - bases/packages.glasskube.dev_packages.yaml
+  - bases/packages.glasskube.dev_packageinfos.yaml
+  - bases/packages.glasskube.dev_packagerepositories.yaml
+  - bases/packages.glasskube.dev_clusterpackages.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
 patches:
-# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
-# patches here are for enabling the conversion webhook for each CRD
-- path: patches/webhook_in_packages.yaml
+  # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
+  # patches here are for enabling the conversion webhook for each CRD
+  - path: patches/webhook_in_packages.yaml
+  - path: patches/webhook_in_clusterpackages.yaml
 #- path: patches/webhook_in_packageinfos.yaml
 #+kubebuilder:scaffold:crdkustomizewebhookpatch
 
@@ -19,10 +21,11 @@ patches:
 #- path: patches/cainjection_in_packages.yaml
 #- path: patches/cainjection_in_packageinfos.yaml
 #- path: patches/cainjection_in_packagerepositories.yaml
+#- path: patches/cainjection_in_clusterpackages.yaml
 #+kubebuilder:scaffold:crdkustomizecainjectionpatch
 
 # [WEBHOOK] To enable webhook, uncomment the following section
 # the following config is for teaching kustomize how to do kustomization for CRDs.
 
 configurations:
-- kustomizeconfig.yaml
+  - kustomizeconfig.yaml
diff --git a/config/crd/patches/cainjection_in_clusterpackages.yaml b/config/crd/patches/cainjection_in_clusterpackages.yaml
new file mode 100644
index 0000000..6ec8a0e
--- /dev/null
+++ b/config/crd/patches/cainjection_in_clusterpackages.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
+  name: clusterpackages.packages.glasskube.dev
diff --git a/config/crd/patches/webhook_in_clusterpackages.yaml b/config/crd/patches/webhook_in_clusterpackages.yaml
new file mode 100644
index 0000000..2305ec5
--- /dev/null
+++ b/config/crd/patches/webhook_in_clusterpackages.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: clusterpackages.packages.glasskube.dev
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/config/rbac/clusterpackage_editor_role.yaml b/config/rbac/clusterpackage_editor_role.yaml
new file mode 100644
index 0000000..a07c15c
--- /dev/null
+++ b/config/rbac/clusterpackage_editor_role.yaml
@@ -0,0 +1,27 @@
+# permissions for end users to edit clusterpackages.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/name: glasskube
+    app.kubernetes.io/managed-by: kustomize
+  name: clusterpackage-editor-role
+rules:
+- apiGroups:
+  - packages.glasskube.dev
+  resources:
+  - clusterpackages
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - packages.glasskube.dev
+  resources:
+  - clusterpackages/status
+  verbs:
+  - get
diff --git a/config/rbac/clusterpackage_viewer_role.yaml b/config/rbac/clusterpackage_viewer_role.yaml
new file mode 100644
index 0000000..4eb3418
--- /dev/null
+++ b/config/rbac/clusterpackage_viewer_role.yaml
@@ -0,0 +1,23 @@
+# permissions for end users to view clusterpackages.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/name: glasskube
+    app.kubernetes.io/managed-by: kustomize
+  name: clusterpackage-viewer-role
+rules:
+- apiGroups:
+  - packages.glasskube.dev
+  resources:
+  - clusterpackages
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - packages.glasskube.dev
+  resources:
+  - clusterpackages/status
+  verbs:
+  - get
diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml
index 0984e3d..02eddbb 100644
--- a/config/rbac/kustomization.yaml
+++ b/config/rbac/kustomization.yaml
@@ -20,5 +20,9 @@ resources:
 # default, aiding admins in cluster management. Those roles are
 # not used by the Project itself. You can comment the following lines
 # if you do not want those helpers be installed with your Project.
+- clusterpackage_editor_role.yaml
+- clusterpackage_viewer_role.yaml
 - packagerepository_editor_role.yaml
 - packagerepository_viewer_role.yaml
+
+
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 159f371..004df9a 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -31,6 +31,32 @@ rules:
   - list
   - update
   - watch
+- apiGroups:
+  - packages.glasskube.dev
+  resources:
+  - clusterpackages
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - packages.glasskube.dev
+  resources:
+  - clusterpackages/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - packages.glasskube.dev
+  resources:
+  - clusterpackages/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - packages.glasskube.dev
   resources:
diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml
index 92192b5..10c4b5c 100644
--- a/config/samples/kustomization.yaml
+++ b/config/samples/kustomization.yaml
@@ -3,4 +3,5 @@ resources:
 - packages_v1alpha1_package.yaml
 - packages_v1alpha1_packageinfo.yaml
 - packages_v1alpha1_packagerepository.yaml
+- packages_v1alpha1_clusterpackage.yaml
 #+kubebuilder:scaffold:manifestskustomizesamples
diff --git a/config/samples/packages_v1alpha1_clusterpackage.yaml b/config/samples/packages_v1alpha1_clusterpackage.yaml
new file mode 100644
index 0000000..53c02f2
--- /dev/null
+++ b/config/samples/packages_v1alpha1_clusterpackage.yaml
@@ -0,0 +1,9 @@
+apiVersion: packages.glasskube.dev/v1alpha1
+kind: ClusterPackage
+metadata:
+  labels:
+    app.kubernetes.io/name: glasskube
+    app.kubernetes.io/managed-by: kustomize
+  name: clusterpackage-sample
+spec:
+  # TODO(user): Add fields here
diff --git a/config/webhook-local/webhook_manifests_patch.yaml b/config/webhook-local/webhook_manifests_patch.yaml
index 97de453..910bc24 100644
--- a/config/webhook-local/webhook_manifests_patch.yaml
+++ b/config/webhook-local/webhook_manifests_patch.yaml
@@ -7,3 +7,7 @@ webhooks:
       service:
         port: 9443
     name: vpackage.kb.io
+  - clientConfig:
+      service:
+        port: 9443
+    name: vclusterpackage.kb.io
diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml
index f2ec5cc..66fc429 100644
--- a/config/webhook/manifests.yaml
+++ b/config/webhook/manifests.yaml
@@ -4,6 +4,27 @@ kind: ValidatingWebhookConfiguration
 metadata:
   name: validating-webhook-configuration
 webhooks:
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: webhook-service
+      namespace: system
+      path: /validate-packages-glasskube-dev-v1alpha1-clusterpackage
+  failurePolicy: Fail
+  name: vclusterpackage.kb.io
+  rules:
+  - apiGroups:
+    - packages.glasskube.dev
+    apiVersions:
+    - v1alpha1
+    operations:
+    - CREATE
+    - UPDATE
+    - DELETE
+    resources:
+    - clusterpackages
+  sideEffects: None
 - admissionReviewVersions:
   - v1
   clientConfig:
diff --git a/internal/adapter/controllerruntime/package_client_adapter.go b/internal/adapter/controllerruntime/package_client_adapter.go
index a625f1b..aae145e 100644
--- a/internal/adapter/controllerruntime/package_client_adapter.go
+++ b/internal/adapter/controllerruntime/package_client_adapter.go
@@ -31,17 +31,26 @@ func (a *ControllerRuntimeAdapter) GetPackageInfo(ctx context.Context, pkgInfoNa
 	}
 }
 
-func (a *ControllerRuntimeAdapter) ListPackages(ctx context.Context) (*v1alpha1.PackageList, error) {
+// ListPackages implements adapter.PackageClientAdapter.
+func (a *ControllerRuntimeAdapter) ListPackages(ctx context.Context, namespace string) (*v1alpha1.PackageList, error) {
 	var pkgList v1alpha1.PackageList
+	if err := a.client.List(ctx, &pkgList, &ctrlclient.ListOptions{Namespace: namespace}); err != nil {
+		return nil, err
+	}
+	return &pkgList, nil
+}
+
+func (a *ControllerRuntimeAdapter) ListClusterPackages(ctx context.Context) (*v1alpha1.ClusterPackageList, error) {
+	var pkgList v1alpha1.ClusterPackageList
 	if err := a.client.List(ctx, &pkgList, &ctrlclient.ListOptions{}); err != nil {
 		return nil, err
 	}
 	return &pkgList, nil
 }
 
-// GetPackage implements adapter.PackageClientAdapter.
-func (a *ControllerRuntimeAdapter) GetPackage(ctx context.Context, name string) (*v1alpha1.Package, error) {
-	var pkg v1alpha1.Package
+// GetClusterPackage implements adapter.PackageClientAdapter.
+func (a *ControllerRuntimeAdapter) GetClusterPackage(ctx context.Context, name string) (*v1alpha1.ClusterPackage, error) {
+	var pkg v1alpha1.ClusterPackage
 	return &pkg, a.client.Get(ctx, ctrlclient.ObjectKey{Name: name}, &pkg)
 }
 
diff --git a/internal/adapter/goclient/package_client_adapter.go b/internal/adapter/goclient/package_client_adapter.go
index 3e31a75..a92c12b 100644
--- a/internal/adapter/goclient/package_client_adapter.go
+++ b/internal/adapter/goclient/package_client_adapter.go
@@ -28,22 +28,32 @@ func (a *goClientPackageAdapter) GetPackageInfo(ctx context.Context, pkgInfoName
 	}
 }
 
-func (a *goClientPackageAdapter) ListPackages(ctx context.Context) (*v1alpha1.PackageList, error) {
-	var pkgList v1alpha1.PackageList
-	if err := a.pkgClient.Packages().GetAll(ctx, &pkgList); err != nil {
+func (a *goClientPackageAdapter) ListClusterPackages(ctx context.Context) (*v1alpha1.ClusterPackageList, error) {
+	var pkgList v1alpha1.ClusterPackageList
+	if err := a.pkgClient.ClusterPackages().GetAll(ctx, &pkgList); err != nil {
 		return nil, err
 	}
 	return &pkgList, nil
 }
 
-// GetPackage implements adapter.PackageClientAdapter.
-func (a *goClientPackageAdapter) GetPackage(ctx context.Context, name string) (*v1alpha1.Package, error) {
-	var pkg v1alpha1.Package
-	return &pkg, a.pkgClient.Packages().Get(ctx, name, &pkg)
+// GetClusterPackage implements adapter.PackageClientAdapter.
+func (a *goClientPackageAdapter) GetClusterPackage(ctx context.Context, name string) (*v1alpha1.ClusterPackage, error) {
+	var pkg v1alpha1.ClusterPackage
+	return &pkg, a.pkgClient.ClusterPackages().Get(ctx, name, &pkg)
+}
+
+// ListPackages implements adapter.PackageClientAdapter.
+func (a *goClientPackageAdapter) ListPackages(ctx context.Context, namespace string) (*v1alpha1.PackageList, error) {
+	var pkgList v1alpha1.PackageList
+	if err := a.pkgClient.Packages(namespace).GetAll(ctx, &pkgList); err != nil {
+		return nil, err
+	}
+	return &pkgList, nil
 }
 
 // GetPackageRepository implements adapter.PackageClientAdapter.
-func (a *goClientPackageAdapter) GetPackageRepository(ctx context.Context, name string) (*v1alpha1.PackageRepository, error) {
+func (a *goClientPackageAdapter) GetPackageRepository(
+	ctx context.Context, name string) (*v1alpha1.PackageRepository, error) {
 	var repo v1alpha1.PackageRepository
 	return &repo, a.pkgClient.PackageRepositories().Get(ctx, name, &repo)
 }
diff --git a/internal/adapter/types.go b/internal/adapter/types.go
index 8485305..14135de 100644
--- a/internal/adapter/types.go
+++ b/internal/adapter/types.go
@@ -9,8 +9,9 @@ import (
 
 type PackageClientAdapter interface {
 	GetPackageInfo(ctx context.Context, pkgInfoName string) (*v1alpha1.PackageInfo, error)
-	ListPackages(ctx context.Context) (*v1alpha1.PackageList, error)
-	GetPackage(ctx context.Context, name string) (*v1alpha1.Package, error)
+	ListClusterPackages(ctx context.Context) (*v1alpha1.ClusterPackageList, error)
+	GetClusterPackage(ctx context.Context, name string) (*v1alpha1.ClusterPackage, error)
+	ListPackages(ctx context.Context, namespace string) (*v1alpha1.PackageList, error)
 	ListPackageRepositories(ctx context.Context) (*v1alpha1.PackageRepositoryList, error)
 	GetPackageRepository(ctx context.Context, name string) (*v1alpha1.PackageRepository, error)
 }
diff --git a/internal/clientutils/autoupdate.go b/internal/clientutils/autoupdate.go
index a1fda4a..3589c02 100644
--- a/internal/clientutils/autoupdate.go
+++ b/internal/clientutils/autoupdate.go
@@ -1,11 +1,11 @@
 package clientutils
 
 import (
-	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 )
 
-func AutoUpdateString(pkg *v1alpha1.Package, disabledStr string) string {
-	if pkg != nil {
+func AutoUpdateString(pkg ctrlpkg.Package, disabledStr string) string {
+	if !pkg.IsNil() {
 		if pkg.AutoUpdatesEnabled() {
 			return "Enabled"
 		}
diff --git a/internal/controller/clusterpackage_controller.go b/internal/controller/clusterpackage_controller.go
new file mode 100644
index 0000000..d7e723c
--- /dev/null
+++ b/internal/controller/clusterpackage_controller.go
@@ -0,0 +1,75 @@
+/*
+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 controller
+
+import (
+	"context"
+
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
+)
+
+// ClusterPackageReconciler reconciles a ClusterPackage object
+type ClusterPackageReconciler struct {
+	PackageReconcilerCommon
+}
+
+//+kubebuilder:rbac:groups=packages.glasskube.dev,resources=clusterpackages,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=packages.glasskube.dev,resources=clusterpackages/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=packages.glasskube.dev,resources=clusterpackages/finalizers,verbs=update
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+// TODO(user): Modify the Reconcile function to compare the state specified by
+// the ClusterPackage object against the actual cluster state, and then
+// perform operations to make the cluster state reflect the state specified by
+// the user.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile
+func (r *ClusterPackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	var pkg v1alpha1.ClusterPackage
+	if err := r.Get(ctx, req.NamespacedName, &pkg); err != nil {
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	return r.reconcile(ctx, &pkg)
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *ClusterPackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	if bld, err := r.baseSetup(mgr, &v1alpha1.ClusterPackage{}, r); err != nil {
+		return err
+	} else {
+		return bld.Complete(r)
+	}
+}
+
+func (r *ClusterPackageReconciler) ListPackages(ctx context.Context) ([]ctrlpkg.Package, error) {
+	var l v1alpha1.ClusterPackageList
+	if err := r.Client.List(ctx, &l); err != nil {
+		return nil, err
+	}
+	res := make([]ctrlpkg.Package, len(l.Items))
+	for i := range l.Items {
+		res[i] = &l.Items[i]
+	}
+	return res, nil
+}
diff --git a/internal/controller/common.go b/internal/controller/common.go
new file mode 100644
index 0000000..285d498
--- /dev/null
+++ b/internal/controller/common.go
@@ -0,0 +1,705 @@
+package controller
+
+import (
+	"context"
+	"fmt"
+	"slices"
+	"strings"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	packagesv1alpha1 "github.com/glasskube/glasskube/api/v1alpha1"
+	ctrladapter "github.com/glasskube/glasskube/internal/adapter/controllerruntime"
+	"github.com/glasskube/glasskube/internal/controller/conditions"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
+	"github.com/glasskube/glasskube/internal/controller/labels"
+	"github.com/glasskube/glasskube/internal/controller/owners"
+	ownerutils "github.com/glasskube/glasskube/internal/controller/owners/utils"
+	"github.com/glasskube/glasskube/internal/controller/requeue"
+	"github.com/glasskube/glasskube/internal/controller/watch"
+	"github.com/glasskube/glasskube/internal/dependency"
+	"github.com/glasskube/glasskube/internal/manifest"
+	"github.com/glasskube/glasskube/internal/manifest/result"
+	"github.com/glasskube/glasskube/internal/manifestvalues"
+	"github.com/glasskube/glasskube/internal/names"
+	repoclient "github.com/glasskube/glasskube/internal/repo/client"
+	"github.com/glasskube/glasskube/internal/telemetry"
+	"github.com/glasskube/glasskube/internal/util"
+	"github.com/glasskube/glasskube/pkg/condition"
+	"go.uber.org/multierr"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/tools/record"
+	"k8s.io/client-go/util/retry"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+const (
+	packageDeletionFinalizer = "packages.glasskube.dev/packageDeletion"
+)
+
+type PackageReconcilerCommon struct {
+	client.Client
+	record.EventRecorder
+	*owners.OwnerManager
+	RepoClientset     repoclient.RepoClientset
+	ValueResolver     *manifestvalues.Resolver
+	Scheme            *runtime.Scheme
+	ManifestAdapter   manifest.ManifestAdapter
+	HelmAdapter       manifest.ManifestAdapter
+	KustomizeAdapter  manifest.ManifestAdapter
+	DependencyManager *dependency.DependendcyManager
+}
+
+func (r *PackageReconcilerCommon) baseSetup(
+	mgr ctrl.Manager, object ctrlpkg.Package, lister watch.PackageLister) (*builder.Builder, error) {
+
+	if r.OwnerManager == nil {
+		r.OwnerManager = owners.NewOwnerManager(r.Scheme)
+	}
+	if r.ValueResolver == nil {
+		r.ValueResolver = manifestvalues.NewResolver(
+			ctrladapter.NewPackageClientAdapter(r.Client),
+			ctrladapter.NewKubernetesClientAdapter(r.Client),
+		)
+	}
+
+	controllerBuilder := ctrl.NewControllerManagedBy(mgr).
+		For(object).
+		Watches(&v1alpha1.PackageInfo{},
+			watch.EnqueueRequestsFromOwnedResource(r.Scheme, lister, watch.OwnedPackageInfos)).
+		Watches(&v1alpha1.ClusterPackage{},
+			watch.EnqueueRequestsFromOwnedResource(r.Scheme, lister, watch.OwnedPackages))
+
+	if err := r.InitAdapters(controllerBuilder); err != nil {
+		return nil, err
+	}
+	return controllerBuilder, nil
+}
+
+func (r *PackageReconcilerCommon) InitAdapters(builder *builder.Builder) error {
+	for _, adapter := range []manifest.ManifestAdapter{r.HelmAdapter, r.KustomizeAdapter, r.ManifestAdapter} {
+		if adapter != nil {
+			if err := adapter.ControllerInit(builder, r.Client, r.Scheme); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (r *PackageReconcilerCommon) reconcile(ctx context.Context, pkg ctrlpkg.Package) (ctrl.Result, error) {
+	prc := &PackageReconcilationContext{PackageReconcilerCommon: r, pkg: pkg}
+	log := ctrl.LoggerFrom(ctx)
+
+	if !pkg.GetDeletionTimestamp().IsZero() {
+		return prc.reconcileAfterDeletion(ctx)
+	}
+
+	telemetry.ForOperator().ReconcilePackage(prc.pkg)
+	prc.ensureFinalizer()
+
+	if err := prc.ensurePackageInfo(ctx); err != nil {
+		return requeue.Always(ctx, err)
+	}
+
+	if meta.IsStatusConditionTrue(prc.pi.Status.Conditions, string(condition.Ready)) {
+		log.V(1).Info("PackageInfo is ready", "packageInfo", prc.pi.Name)
+		return prc.reconcilePackageInfoReady(ctx)
+	} else if meta.IsStatusConditionFalse(prc.pi.Status.Conditions, string(condition.Ready)) {
+		packageInfoCondition := meta.FindStatusCondition(prc.pi.Status.Conditions, string(condition.Ready))
+		prc.setShouldUpdate(
+			conditions.SetFailed(ctx, prc.EventRecorder, prc.pkg, &prc.pkg.GetStatus().Conditions,
+				condition.Reason(packageInfoCondition.Reason), packageInfoCondition.Message))
+		return prc.finalize(ctx)
+	} else {
+		prc.setShouldUpdate(
+			conditions.SetUnknown(ctx, &prc.pkg.GetStatus().Conditions, condition.Pending, "PackageInfo status is unknown"))
+		return prc.finalize(ctx)
+	}
+}
+
+type PackageReconcilationContext struct {
+	*PackageReconcilerCommon
+	pkg                   ctrlpkg.Package
+	pi                    *v1alpha1.PackageInfo
+	isSuccess             bool
+	shouldUpdateStatus    bool
+	shouldUpdateResource  bool
+	currentOwnedResources []v1alpha1.OwnedResourceRef
+	currentOwnedPackages  []v1alpha1.OwnedResourceRef
+}
+
+func (r *PackageReconcilationContext) setShouldUpdate(value bool) {
+	r.shouldUpdateStatus = r.shouldUpdateStatus || value
+}
+
+func (r *PackageReconcilationContext) reconcilePackageInfoReady(ctx context.Context) (ctrl.Result, error) {
+	piManifest := r.pi.Status.Manifest
+	if piManifest == nil {
+		r.setShouldUpdate(
+			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+				condition.UnsupportedFormat, "manifest must not be nil"))
+		return r.finalizeNoRequeue(ctx)
+	}
+
+	if !r.ensureDependencies(ctx) {
+		return r.finalize(ctx)
+	}
+
+	var patches []manifestvalues.TargetPatch
+	if resolvedValues, err := r.ValueResolver.Resolve(ctx, r.pkg.GetSpec().Values); err != nil {
+		r.setShouldUpdate(
+			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+				condition.ValueConfigurationInvalid, err.Error()))
+		return r.finalizeWithError(ctx, err)
+	} else if err := manifestvalues.ValidateResolvedValues(*piManifest, resolvedValues); err != nil {
+		r.setShouldUpdate(
+			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+				condition.ValueConfigurationInvalid, err.Error()))
+		return r.finalizeWithError(ctx, err)
+	} else if p, err := manifestvalues.GeneratePatches(*piManifest, resolvedValues); err != nil {
+		r.setShouldUpdate(
+			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+				condition.InstallationFailed, err.Error()))
+		return r.finalizeWithError(ctx, err)
+	} else {
+		patches = p
+	}
+
+	// First, collect the adapters for all included manifests and ensure that they are supported.
+	// If one manifest type is not supported, no action must be performed!
+
+	var adaptersToRun []manifest.ManifestAdapter
+	if len(piManifest.Manifests) > 0 {
+		if r.ManifestAdapter == nil {
+			r.setShouldUpdate(
+				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+					condition.UnsupportedFormat, "manifests not supported"))
+			return r.finalizeNoRequeue(ctx)
+		}
+		adaptersToRun = append(adaptersToRun, r.ManifestAdapter)
+	}
+	if piManifest.Kustomize != nil {
+		if r.KustomizeAdapter == nil {
+			r.setShouldUpdate(
+				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+					condition.UnsupportedFormat, "kustomize not supported"))
+			return r.finalizeNoRequeue(ctx)
+		}
+		adaptersToRun = append(adaptersToRun, r.KustomizeAdapter)
+	}
+	if piManifest.Helm != nil {
+		if r.HelmAdapter == nil {
+			r.setShouldUpdate(
+				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+					condition.UnsupportedFormat, "helm not supported"))
+			return r.finalizeNoRequeue(ctx)
+		}
+		adaptersToRun = append(adaptersToRun, r.HelmAdapter)
+	}
+
+	results := make([]result.ReconcileResult, 0, len(adaptersToRun))
+	var errs error
+	for _, adapter := range adaptersToRun {
+		if result, err := adapter.Reconcile(ctx, r.pkg, piManifest, patches); err != nil {
+			errs = multierr.Append(errs, err)
+		} else {
+			results = append(results, *result)
+			ownerutils.Add(&r.currentOwnedResources, result.OwnedResources...)
+		}
+	}
+
+	if errs != nil {
+		r.setShouldUpdate(
+			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+				condition.InstallationFailed, errs.Error()))
+		return r.finalizeWithError(ctx, errs)
+	} else if !r.handleAdapterResults(ctx, results) {
+		return r.finalize(ctx)
+	} else {
+		r.afterSuccess(ctx, results)
+		return r.finalize(ctx)
+	}
+}
+
+func (r *PackageReconcilationContext) reconcileAfterDeletion(ctx context.Context) (ctrl.Result, error) {
+	log := ctrl.LoggerFrom(ctx)
+
+	r.setShouldUpdate(conditions.SetUnknown(ctx, &r.pkg.GetStatus().Conditions,
+		condition.Pending, "Package is being deleted"))
+
+	if r.shouldUpdateStatus {
+		telemetry.ForOperator().ReportDelete(r.pkg)
+	}
+
+	if slices.Contains(r.pkg.GetFinalizers(), "packages.glasskube.dev/packageDeletion") {
+		var err error
+		if len(r.pkg.GetStatus().OwnedPackages) != 0 {
+			multierr.AppendInto(&err, r.pruneOwnedPackages(ctx, true))
+			log.Info("waiting for deletion of required packages")
+		} else if len(r.pkg.GetStatus().OwnedPackageInfos) != 0 {
+			multierr.AppendInto(&err, r.pruneOwnedPackageInfos(ctx, true))
+			log.Info("waiting for deletion of package infos")
+		} else {
+			r.pkg.SetFinalizers(util.DeleteAll(r.pkg.GetFinalizers(), packageDeletionFinalizer))
+			r.shouldUpdateResource = true
+		}
+
+		if err != nil {
+			return r.finalizeWithError(ctx, err)
+		}
+	}
+
+	return r.finalizeNoRequeue(ctx)
+}
+
+func (r *PackageReconcilationContext) ensureFinalizer() {
+	if !slices.Contains(r.pkg.GetFinalizers(), packageDeletionFinalizer) {
+		r.pkg.SetFinalizers(append(r.pkg.GetFinalizers(), packageDeletionFinalizer))
+		r.shouldUpdateResource = true
+	}
+}
+
+func (r *PackageReconcilationContext) ensureDependencies(ctx context.Context) bool {
+	log := ctrl.LoggerFrom(ctx)
+	log.V(1).Info("ensuring dependencies", "dependencies", r.pi.Status.Manifest.Dependencies)
+
+	var failed []string
+	if result, err := r.DependencyManager.Validate(ctx, r.pi.Status.Manifest,
+		r.pkg.GetSpec().PackageInfo.Version); err != nil {
+		r.setShouldUpdate(
+			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+				condition.InstallationFailed, fmt.Sprintf("error validating dependencies: %v", err)))
+		return false
+	} else if result.Status == dependency.ValidationResultStatusResolvable {
+		for _, requirement := range result.Requirements {
+			if requirement.Transitive {
+				// Only direct dependencies should be touched in the context of the reconciliation of a package.
+				continue
+			}
+			newPkg := &packagesv1alpha1.ClusterPackage{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      requirement.Name,
+					Namespace: r.pkg.GetNamespace(),
+				},
+			}
+
+			newPkg.SetInstalledAsDependency(true)
+
+			repositories, err := r.RepoClientset.Meta().GetReposForPackage(requirement.Name)
+			if err != nil {
+				log.Error(err, "could not find repos for package", "required", requirement.Name)
+				failed = append(failed, requirement.Name)
+				continue
+			}
+
+			var repositoryName string
+			switch len(repositories) {
+			case 0:
+				log.Error(err, "could not find package in any repo", "required", requirement.Name)
+				failed = append(failed, requirement.Name)
+				continue
+			case 1:
+				repositoryName = repositories[0].Name
+			default:
+				log.Error(err, "dependency in multiple repos is not supported yet", "required", requirement.Name)
+				failed = append(failed, requirement.Name)
+				continue
+			}
+
+			if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, newPkg, func() error {
+				newPkg.Spec = packagesv1alpha1.PackageSpec{
+					PackageInfo: packagesv1alpha1.PackageInfoTemplate{
+						Name:           requirement.Name,
+						Version:        requirement.Version,
+						RepositoryName: repositoryName,
+					},
+				}
+				return nil
+			}); err != nil {
+				log.Error(err, "Failed to create required package", "required", requirement.Name)
+				failed = append(failed, requirement.Name)
+			}
+		}
+		if len(failed) > 0 {
+			r.setShouldUpdate(
+				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+					condition.InstallationFailed, fmt.Sprintf("required package(s) not installed: %v", strings.Join(failed, ","))))
+			return false
+		}
+	} else if result.Status == dependency.ValidationResultStatusConflict {
+		var parts []string
+		for _, c := range result.Conflicts {
+			parts = append(parts, fmt.Sprintf("need version %v of %v but found %v",
+				c.Required.Version, c.Actual.Name, c.Actual.Version))
+		}
+		r.setShouldUpdate(
+			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+				condition.InstallationFailed, fmt.Sprintf("conflicting dependencies: %v", strings.Join(parts, ","))))
+		return false
+	}
+
+	var ownedPackages []packagesv1alpha1.OwnedResourceRef
+	var waitingFor []string
+	// if all requirements fulfilled, status can be checked
+	for _, dep := range r.pi.Status.Manifest.Dependencies {
+		var requiredPkg packagesv1alpha1.ClusterPackage
+		if err := r.Get(ctx, types.NamespacedName{Name: dep.Name}, &requiredPkg); err != nil {
+			if apierrors.IsNotFound(err) {
+				waitingFor = append(waitingFor, dep.Name)
+			} else {
+				message := fmt.Sprintf("failed to get required package %v: %v", dep.Name, err)
+				r.setShouldUpdate(
+					conditions.SetFailed(ctx, r.EventRecorder, r.pkg,
+						&r.pkg.GetStatus().Conditions, condition.InstallationFailed, message))
+				return false
+			}
+		} else {
+			if owned, err := ownerutils.ToOwnedResourceRef(r.Scheme, &requiredPkg); err != nil {
+				log.Error(err, "Failed to create OwnedResourceRef", "package", requiredPkg)
+				failed = append(failed, dep.Name)
+			} else {
+				ownedPackages = append(ownedPackages, owned)
+			}
+			if meta.IsStatusConditionTrue(requiredPkg.Status.Conditions, string(condition.Failed)) {
+				failed = append(failed, requiredPkg.Name)
+			} else if !meta.IsStatusConditionTrue(requiredPkg.Status.Conditions, string(condition.Ready)) {
+				waitingFor = append(waitingFor, requiredPkg.Name)
+			}
+		}
+	}
+
+	if len(failed) > 0 {
+		message := fmt.Sprintf("required package(s) not installed: %v", strings.Join(failed, ","))
+		r.setShouldUpdate(conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+			condition.InstallationFailed, message))
+		return false
+	}
+
+	if len(waitingFor) > 0 {
+		message := fmt.Sprintf("waiting for required package(s) %v", strings.Join(waitingFor, ","))
+		r.setShouldUpdate(
+			conditions.SetUnknown(ctx, &r.pkg.GetStatus().Conditions, condition.Pending, message))
+		return false
+	}
+
+	ownerutils.Add(&r.currentOwnedPackages, ownedPackages...)
+
+	return true
+}
+
+func (r *PackageReconcilationContext) ensurePackageInfo(ctx context.Context) error {
+	packageInfo := packagesv1alpha1.PackageInfo{
+		ObjectMeta: metav1.ObjectMeta{Name: names.PackageInfoName(r.pkg)},
+	}
+	log := ctrl.LoggerFrom(ctx).WithValues("PackageInfo", packageInfo.Name)
+
+	log.V(1).Info("ensuring PackageInfo")
+	result, err := controllerutil.CreateOrUpdate(ctx, r.Client, &packageInfo, func() error {
+		packageInfo.Spec = packagesv1alpha1.PackageInfoSpec{
+			Name:           r.pkg.GetSpec().PackageInfo.Name,
+			Version:        r.pkg.GetSpec().PackageInfo.Version,
+			RepositoryName: r.pkg.GetSpec().PackageInfo.RepositoryName,
+		}
+		return nil
+	})
+	if err != nil {
+		return fmt.Errorf("could not create or update PackageInfo: %w", err)
+	}
+
+	log.V(1).Info("ensured PackageInfo", "result", result)
+
+	// After CreateOrUpdate, PackageInfo does not always have correct TypeMeta.
+	// To fix this, we Get it again here.
+	// TODO: Find out why this is even needed.
+	if result == controllerutil.OperationResultCreated {
+		err := retry.OnError(retry.DefaultBackoff, apierrors.IsNotFound, func() error {
+			return r.Get(ctx, client.ObjectKeyFromObject(&packageInfo), &packageInfo)
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	if changed, err := ownerutils.AddOwnedResourceRef(
+		r.Scheme, &r.pkg.GetStatus().OwnedPackageInfos, &packageInfo); err != nil {
+		log.Error(err, "could not add PackageInfo to owned resources")
+	} else {
+		r.setShouldUpdate(changed)
+	}
+
+	r.pi = &packageInfo
+	return nil
+}
+
+func (r *PackageReconcilationContext) handleAdapterResults(ctx context.Context, results []result.ReconcileResult) bool {
+	var firstFailed *result.ReconcileResult
+	var firstWaiting *result.ReconcileResult
+	for i, result := range results {
+		if result.IsFailed() && firstFailed == nil {
+			firstFailed = &results[i]
+		} else if result.IsWaiting() && firstWaiting == nil {
+			firstWaiting = &results[i]
+		}
+	}
+
+	if firstFailed != nil {
+		r.setShouldUpdate(
+			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+				condition.InstallationFailed, firstFailed.Message))
+		return false
+	} else if firstWaiting != nil {
+		r.setShouldUpdate(
+			conditions.SetUnknown(ctx, &r.pkg.GetStatus().Conditions, condition.Pending, firstWaiting.Message))
+		return false
+	} else {
+		return true
+	}
+}
+
+func (r *PackageReconcilationContext) afterSuccess(ctx context.Context, results []result.ReconcileResult) {
+	reason := condition.UpToDate
+	message := "PackageInfo has nothing to apply (no helm or kustomize manifest present)"
+	if len(results) > 0 {
+		messages := make([]string, 0, len(results))
+		for _, result := range results {
+			messages = append(messages, result.Message)
+		}
+		reason = condition.InstallationSucceeded
+		message = strings.Join(messages, "\n")
+	}
+
+	r.setShouldUpdate(
+		conditions.SetReady(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions, reason, message))
+	r.setShouldUpdate(r.pkg.GetStatus().Version != r.pi.Status.Version)
+	r.pkg.GetStatus().Version = r.pi.Status.Version
+	r.isSuccess = true
+}
+
+func (r *PackageReconcilationContext) finalize(ctx context.Context) (ctrl.Result, error) {
+	return requeue.Always(ctx, r.actualFinalize(ctx))
+}
+
+func (r *PackageReconcilationContext) finalizeNoRequeue(ctx context.Context) (ctrl.Result, error) {
+	return requeue.OnError(ctx, r.actualFinalize(ctx))
+}
+
+func (r *PackageReconcilationContext) finalizeWithError(ctx context.Context, err error) (ctrl.Result, error) {
+	return requeue.Always(ctx, multierr.Append(err, r.actualFinalize(ctx)))
+}
+
+func (r *PackageReconcilationContext) actualFinalize(ctx context.Context) error {
+	log := ctrl.LoggerFrom(ctx)
+
+	r.setShouldUpdate(ownerutils.Add(&r.pkg.GetStatus().OwnedResources, r.currentOwnedResources...))
+	r.setShouldUpdate(ownerutils.Add(&r.pkg.GetStatus().OwnedPackages, r.currentOwnedPackages...))
+
+	var errs error
+	if r.isSuccess {
+		if err := r.cleanup(ctx); err != nil {
+			r.setShouldUpdate(
+				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.GetStatus().Conditions,
+					condition.InstallationFailed, fmt.Sprintf("cleanup failed: %v", err)))
+			errs = multierr.Append(errs, err)
+		}
+		log.V(1).Info("cleanup done")
+	}
+
+	if r.shouldUpdateStatus {
+		if err := r.Status().Update(ctx, r.pkg); err != nil {
+			log.Error(err, "package status update failed")
+			errs = multierr.Append(errs, err)
+		} else {
+			log.Info("package status updated")
+		}
+	} else if r.shouldUpdateResource {
+		if err := r.Update(ctx, r.pkg); err != nil {
+			log.Error(err, "package update failed")
+			errs = multierr.Append(errs, err)
+		} else {
+			log.Info("package updated")
+		}
+	}
+
+	return errs
+}
+
+func (r *PackageReconcilationContext) cleanup(ctx context.Context) error {
+	return multierr.Combine(
+		r.pruneOwnedResources(ctx),
+		r.pruneOwnedPackageInfos(ctx, false),
+		r.pruneOwnedPackages(ctx, false),
+	)
+}
+
+func (r *PackageReconcilationContext) pruneOwnedResources(ctx context.Context) error {
+	log := ctrl.LoggerFrom(ctx)
+	var errs error
+	ownedResourcesCopy := r.pkg.GetStatus().OwnedResources[:]
+OuterLoop:
+	for _, ref := range r.pkg.GetStatus().OwnedResources {
+		for _, newRef := range r.currentOwnedResources {
+			if ownerutils.RefersToSameResource(ref, newRef) {
+				// ref is still an owned resource
+				continue OuterLoop
+			}
+		}
+		// ref is no longer an owned resource.
+		// check if it is managed by the operator and delete it if it is.
+		obj := ownerutils.OwnedResourceRefToObject(ref)
+		if err := r.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
+			if !apierrors.IsNotFound(err) {
+				multierr.AppendInto(&errs, fmt.Errorf("could get resource during pruning: %w", err))
+			}
+		} else if labels.IsManaged(obj) {
+			if err := r.Delete(ctx, obj); err != nil && !apierrors.IsNotFound(err) {
+				multierr.AppendInto(&errs, fmt.Errorf("could not prune resource: %w", err))
+			} else {
+				log.V(1).Info("pruned resource", "reference", ref)
+				r.setShouldUpdate(ownerutils.Remove(&ownedResourcesCopy, ref))
+			}
+		} else {
+			log.V(1).Info("skipped pruning unmanaged resource", "reference", ref)
+			r.setShouldUpdate(ownerutils.Remove(&ownedResourcesCopy, ref))
+		}
+	}
+	r.pkg.GetStatus().OwnedResources = ownedResourcesCopy
+	return errs
+}
+
+func (r *PackageReconcilationContext) pruneOwnedPackageInfos(ctx context.Context, all bool) error {
+	log := ctrl.LoggerFrom(ctx)
+
+	allPackages, err := r.getAllPackagesAndClusterPackages(ctx)
+	if err != nil {
+		return err
+	}
+
+	var compositeErr error
+	for _, ref := range r.pkg.GetStatus().OwnedPackageInfos {
+		if !all && ref.Name == names.PackageInfoName(r.pkg) {
+			continue
+		}
+
+		// find other packages that require this
+		stillUsed := false
+	loopAllPackages:
+		for _, pkg := range allPackages {
+			if pkg.GetName() == r.pkg.GetName() && pkg.GetNamespace() == r.pkg.GetNamespace() {
+				// skip current pkg
+				continue
+			}
+			for _, otherRef := range pkg.GetStatus().OwnedPackageInfos {
+				if otherRef.Name == ref.Name {
+					stillUsed = true
+					break loopAllPackages
+				}
+			}
+		}
+
+		if !stillUsed {
+			log.V(1).Info("deleting old package info", "PackageInfo", ref.Name)
+			if err := r.Delete(ctx, ownerutils.OwnedResourceRefToObject(ref)); client.IgnoreNotFound(err) != nil {
+				compositeErr = multierr.Append(compositeErr, err)
+				continue
+			}
+		}
+
+		// Remove the PackageInfo from the owned PackageInfos field of pkg
+		ownerutils.RemoveOwnedResourceRef(&r.pkg.GetStatus().OwnedPackageInfos, ref)
+		r.setShouldUpdate(true)
+	}
+	return compositeErr
+}
+
+func (r *PackageReconcilationContext) pruneOwnedPackages(ctx context.Context, all bool) error {
+	log := ctrl.LoggerFrom(ctx)
+	var errs error
+	ownedPackagesCopy := r.pkg.GetStatus().OwnedPackages[:]
+
+	allPackages, err := r.getAllPackagesAndClusterPackages(ctx)
+	if err != nil {
+		return err
+	}
+
+OuterLoop:
+	for _, ref := range r.pkg.GetStatus().OwnedPackages {
+		if !all {
+			for _, newRef := range r.currentOwnedPackages {
+				if ownerutils.RefersToSameResource(ref, newRef) {
+					continue OuterLoop
+				}
+			}
+		}
+
+		stillUsed := false
+	AllPackagesLoop:
+		for _, otherPkg := range allPackages {
+			if !otherPkg.GetDeletionTimestamp().IsZero() ||
+				(otherPkg.GetName() == r.pkg.GetName() && otherPkg.GetNamespace() == r.pkg.GetNamespace()) {
+				continue
+			}
+			for _, otherRef := range otherPkg.GetStatus().OwnedPackages {
+				if otherRef.Name == ref.Name {
+					stillUsed = true
+					break AllPackagesLoop
+				}
+			}
+		}
+
+		if !stillUsed {
+			var oldReqPkg v1alpha1.ClusterPackage
+			if err := r.Get(ctx,
+				types.NamespacedName{Name: ref.Name, Namespace: ref.Namespace}, &oldReqPkg); err != nil {
+				if apierrors.IsNotFound(err) {
+					r.setShouldUpdate(ownerutils.Remove(&ownedPackagesCopy, ref))
+				} else {
+					multierr.AppendInto(&errs, fmt.Errorf("failed to get old required package: %w", err))
+					continue OuterLoop
+				}
+			} else if oldReqPkg.InstalledAsDependency() {
+				// remove the old package if we were the only package owning it and it was previously installed as a dependency
+				if err := r.Delete(ctx, ownerutils.OwnedResourceRefToObject(ref),
+					&client.DeleteOptions{PropagationPolicy: util.Pointer(metav1.DeletePropagationForeground)},
+				); err != nil && !apierrors.IsNotFound(err) {
+					errs = multierr.Append(errs, fmt.Errorf("could not prune package: %w", err))
+					continue OuterLoop
+				} else {
+					log.V(1).Info("pruned package", "reference", ref)
+				}
+			}
+		}
+
+		r.setShouldUpdate(ownerutils.Remove(&ownedPackagesCopy, ref))
+	}
+	r.pkg.GetStatus().OwnedPackages = ownedPackagesCopy
+	return errs
+}
+
+func (r *PackageReconcilerCommon) getAllPackagesAndClusterPackages(ctx context.Context) ([]ctrlpkg.Package, error) {
+	var pkgList packagesv1alpha1.PackageList
+	if err := r.List(ctx, &pkgList, &client.ListOptions{}); err != nil {
+		return nil, err
+	}
+
+	var cpkgList packagesv1alpha1.ClusterPackageList
+	if err := r.List(ctx, &cpkgList, &client.ListOptions{}); err != nil {
+		return nil, err
+	}
+
+	allPackages := make([]ctrlpkg.Package, len(pkgList.Items)+len(cpkgList.Items))
+	for i := range pkgList.Items {
+		allPackages[i] = &pkgList.Items[i]
+	}
+	for i := range cpkgList.Items {
+		allPackages[len(pkgList.Items)+i] = &cpkgList.Items[i]
+	}
+
+	return allPackages, nil
+}
diff --git a/internal/controller/ctrlpkg/interface.go b/internal/controller/ctrlpkg/interface.go
new file mode 100644
index 0000000..09b8805
--- /dev/null
+++ b/internal/controller/ctrlpkg/interface.go
@@ -0,0 +1,26 @@
+package ctrlpkg
+
+import (
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// Package may represent a v1alpha1.Package or v1alpha1.ClusterPackage
+type Package interface {
+	metav1.Object
+	runtime.Object
+	schema.ObjectKind
+	AutoUpdatesEnabled() bool
+	SetAutoUpdatesEnabled(enabled bool)
+	InstalledAsDependency() bool
+	SetInstalledAsDependency(value bool)
+	GetSpec() *v1alpha1.PackageSpec
+	GetStatus() *v1alpha1.PackageStatus
+	IsNamespaceScoped() bool
+	IsNil() bool
+}
+
+var _ Package = &v1alpha1.Package{}
+var _ Package = &v1alpha1.ClusterPackage{}
diff --git a/internal/controller/package_controller.go b/internal/controller/package_controller.go
index bbfff91..4a6bb3b 100644
--- a/internal/controller/package_controller.go
+++ b/internal/controller/package_controller.go
@@ -18,51 +18,16 @@ package controller
 
 import (
 	"context"
-	"fmt"
-	"strings"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
-	packagesv1alpha1 "github.com/glasskube/glasskube/api/v1alpha1"
-	ctrladapter "github.com/glasskube/glasskube/internal/adapter/controllerruntime"
-	"github.com/glasskube/glasskube/internal/controller/conditions"
-	"github.com/glasskube/glasskube/internal/controller/labels"
-	"github.com/glasskube/glasskube/internal/controller/owners"
-	ownerutils "github.com/glasskube/glasskube/internal/controller/owners/utils"
-	"github.com/glasskube/glasskube/internal/controller/requeue"
-	"github.com/glasskube/glasskube/internal/dependency"
-	"github.com/glasskube/glasskube/internal/manifest"
-	"github.com/glasskube/glasskube/internal/manifest/result"
-	"github.com/glasskube/glasskube/internal/manifestvalues"
-	"github.com/glasskube/glasskube/internal/names"
-	repoclient "github.com/glasskube/glasskube/internal/repo/client"
-	"github.com/glasskube/glasskube/internal/telemetry"
-	"github.com/glasskube/glasskube/pkg/condition"
-	"go.uber.org/multierr"
-	apierrors "k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/api/meta"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/client-go/tools/record"
-	"k8s.io/client-go/util/retry"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	ctrl "sigs.k8s.io/controller-runtime"
-	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 )
 
 // PackageReconciler reconciles a Package object
 type PackageReconciler struct {
-	client.Client
-	record.EventRecorder
-	*owners.OwnerManager
-	RepoClientset     repoclient.RepoClientset
-	ValueResolver     *manifestvalues.Resolver
-	Scheme            *runtime.Scheme
-	ManifestAdapter   manifest.ManifestAdapter
-	HelmAdapter       manifest.ManifestAdapter
-	KustomizeAdapter  manifest.ManifestAdapter
-	DependencyManager *dependency.DependendcyManager
+	PackageReconcilerCommon
 }
 
 //+kubebuilder:rbac:groups=packages.glasskube.dev,resources=packages,verbs=get;list;watch;create;update;patch;delete
@@ -83,614 +48,31 @@ type PackageReconciler struct {
 // For more details, check Reconcile and its Result here:
 // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcile
 func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	var pkg packagesv1alpha1.Package
-
+	var pkg v1alpha1.Package
 	if err := r.Get(ctx, req.NamespacedName, &pkg); err != nil {
 		return ctrl.Result{}, client.IgnoreNotFound(err)
 	}
 
-	if !pkg.DeletionTimestamp.IsZero() {
-		changed, err := conditions.SetUnknownAndUpdate(ctx, r.Client, &pkg, &pkg.Status.Conditions,
-			condition.Pending, "Package is being deleted")
-		if changed {
-			telemetry.ForOperator().ReportDelete(&pkg)
-		}
-		return ctrl.Result{}, err
-	}
-
-	telemetry.ForOperator().ReconcilePackage(&pkg)
-
-	return r.reconcilePackage(ctx, pkg)
-}
-
-func (r *PackageReconciler) reconcilePackage(ctx context.Context, pkg v1alpha1.Package) (ctrl.Result, error) {
-	return (&PackageReconcilationContext{PackageReconciler: r, pkg: &pkg}).reconcile(ctx)
+	return r.reconcile(ctx, &pkg)
 }
 
 // SetupWithManager sets up the controller with the Manager.
 func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
-	if r.OwnerManager == nil {
-		r.OwnerManager = owners.NewOwnerManager(r.Scheme)
-	}
-	if r.ValueResolver == nil {
-		r.ValueResolver = manifestvalues.NewResolver(
-			ctrladapter.NewPackageClientAdapter(r.Client),
-			ctrladapter.NewKubernetesClientAdapter(r.Client),
-		)
-	}
-	controllerBuilder := ctrl.NewControllerManagedBy(mgr).For(&packagesv1alpha1.Package{})
-	for _, adapter := range []manifest.ManifestAdapter{r.HelmAdapter, r.KustomizeAdapter, r.ManifestAdapter} {
-		if adapter != nil {
-			if err := adapter.ControllerInit(controllerBuilder, r.Client, r.Scheme); err != nil {
-				return err
-			}
-		}
-	}
-	return controllerBuilder.
-		Owns(&packagesv1alpha1.PackageInfo{}, builder.MatchEveryOwner).
-		Owns(&packagesv1alpha1.Package{}, builder.MatchEveryOwner).
-		Complete(r)
-}
-
-type PackageReconcilationContext struct {
-	*PackageReconciler
-	pkg                   *v1alpha1.Package
-	pi                    *v1alpha1.PackageInfo
-	isSuccess             bool
-	shouldUpdateStatus    bool
-	currentOwnedResources []v1alpha1.OwnedResourceRef
-	currentOwnedPackages  []v1alpha1.OwnedResourceRef
-}
-
-func (r *PackageReconcilationContext) setShouldUpdate(value bool) {
-	r.shouldUpdateStatus = r.shouldUpdateStatus || value
-}
-
-func (r *PackageReconcilationContext) reconcile(ctx context.Context) (ctrl.Result, error) {
-	log := ctrl.LoggerFrom(ctx)
-	if err := r.ensurePackageInfo(ctx); err != nil {
-		return requeue.Always(ctx, err)
-	}
-
-	if meta.IsStatusConditionTrue(r.pi.Status.Conditions, string(condition.Ready)) {
-		log.V(1).Info("PackageInfo is ready", "packageInfo", r.pi.Name)
-		return r.reconcilePackageInfoReady(ctx)
-	} else if meta.IsStatusConditionFalse(r.pi.Status.Conditions, string(condition.Ready)) {
-		packageInfoCondition := meta.FindStatusCondition(r.pi.Status.Conditions, string(condition.Ready))
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.Reason(packageInfoCondition.Reason), packageInfoCondition.Message))
-		return r.finalize(ctx)
-	} else {
-		r.setShouldUpdate(
-			conditions.SetUnknown(ctx, &r.pkg.Status.Conditions, condition.Pending, "PackageInfo status is unknown"))
-		return r.finalize(ctx)
-	}
-}
-
-func (r *PackageReconcilationContext) reconcilePackageInfoReady(ctx context.Context) (ctrl.Result, error) {
-	piManifest := r.pi.Status.Manifest
-	if piManifest == nil {
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.UnsupportedFormat, "manifest must not be nil"))
-		return r.finalizeNoRequeue(ctx)
-	}
-
-	if !r.ensureDependencies(ctx) {
-		return r.finalize(ctx)
-	}
-
-	var patches []manifestvalues.TargetPatch
-	if resolvedValues, err := r.ValueResolver.Resolve(ctx, r.pkg.Spec.Values); err != nil {
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.ValueConfigurationInvalid, err.Error()))
-		return r.finalizeWithError(ctx, err)
-	} else if err := manifestvalues.ValidateResolvedValues(*piManifest, resolvedValues); err != nil {
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.ValueConfigurationInvalid, err.Error()))
-		return r.finalizeWithError(ctx, err)
-	} else if p, err := manifestvalues.GeneratePatches(*piManifest, resolvedValues); err != nil {
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.InstallationFailed, err.Error()))
-		return r.finalizeWithError(ctx, err)
-	} else {
-		patches = p
-	}
-
-	// First, collect the adapters for all included manifests and ensure that they are supported.
-	// If one manifest type is not supported, no action must be performed!
-
-	var adaptersToRun []manifest.ManifestAdapter
-	if len(piManifest.Manifests) > 0 {
-		if r.ManifestAdapter == nil {
-			r.setShouldUpdate(
-				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-					condition.UnsupportedFormat, "manifests not supported"))
-			return r.finalizeNoRequeue(ctx)
-		}
-		adaptersToRun = append(adaptersToRun, r.ManifestAdapter)
-	}
-	if piManifest.Kustomize != nil {
-		if r.KustomizeAdapter == nil {
-			r.setShouldUpdate(
-				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-					condition.UnsupportedFormat, "kustomize not supported"))
-			return r.finalizeNoRequeue(ctx)
-		}
-		adaptersToRun = append(adaptersToRun, r.KustomizeAdapter)
-	}
-	if piManifest.Helm != nil {
-		if r.HelmAdapter == nil {
-			r.setShouldUpdate(
-				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-					condition.UnsupportedFormat, "helm not supported"))
-			return r.finalizeNoRequeue(ctx)
-		}
-		adaptersToRun = append(adaptersToRun, r.HelmAdapter)
-	}
-
-	results := make([]result.ReconcileResult, 0, len(adaptersToRun))
-	var errs error
-	for _, adapter := range adaptersToRun {
-		if result, err := adapter.Reconcile(ctx, r.pkg, piManifest, patches); err != nil {
-			errs = multierr.Append(errs, err)
-		} else {
-			results = append(results, *result)
-			ownerutils.Add(&r.currentOwnedResources, result.OwnedResources...)
-		}
-	}
-
-	if errs != nil {
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.InstallationFailed, errs.Error()))
-		return r.finalizeWithError(ctx, errs)
-	} else if !r.handleAdapterResults(ctx, results) {
-		return r.finalize(ctx)
-	} else {
-		r.afterSuccess(ctx, results)
-		return r.finalize(ctx)
-	}
-}
-
-func (r *PackageReconcilationContext) ensureDependencies(ctx context.Context) bool {
-	log := ctrl.LoggerFrom(ctx)
-	log.V(1).Info("ensuring dependencies", "dependencies", r.pi.Status.Manifest.Dependencies)
-
-	var failed []string
-	if result, err := r.DependencyManager.Validate(ctx, r.pi.Status.Manifest, r.pkg.Spec.PackageInfo.Version); err != nil {
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.InstallationFailed, fmt.Sprintf("error validating dependencies: %v", err)))
-		return false
-	} else if result.Status == dependency.ValidationResultStatusResolvable {
-		for _, requirement := range result.Requirements {
-			if requirement.Transitive {
-				// Only direct dependencies should be touched in the context of the reconciliation of a package.
-				continue
-			}
-			newPkg := &packagesv1alpha1.Package{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      requirement.Name,
-					Namespace: r.pkg.Namespace,
-				},
-			}
-
-			repositories, err := r.RepoClientset.Meta().GetReposForPackage(requirement.Name)
-			if err != nil {
-				log.Error(err, "could not find repos for package", "required", requirement.Name)
-				failed = append(failed, requirement.Name)
-				continue
-			}
-
-			var repositoryName string
-			switch len(repositories) {
-			case 0:
-				log.Error(err, "could not find package in any repo", "required", requirement.Name)
-				failed = append(failed, requirement.Name)
-				continue
-			case 1:
-				repositoryName = repositories[0].Name
-			default:
-				log.Error(err, "dependency in multiple repos is not supported yet", "required", requirement.Name)
-				failed = append(failed, requirement.Name)
-				continue
-			}
-
-			if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, newPkg, func() error {
-				if err := r.SetOwner(r.pkg, newPkg, owners.BlockOwnerDeletion); err != nil {
-					return fmt.Errorf("unable to set ownerReference on required package: %w", err)
-				}
-				newPkg.Spec = packagesv1alpha1.PackageSpec{
-					PackageInfo: packagesv1alpha1.PackageInfoTemplate{
-						Name:           requirement.Name,
-						Version:        requirement.Version,
-						RepositoryName: repositoryName,
-					},
-				}
-				return nil
-			}); err != nil {
-				log.Error(err, "Failed to create required package", "required", requirement.Name)
-				failed = append(failed, requirement.Name)
-			}
-		}
-		if len(failed) > 0 {
-			r.setShouldUpdate(
-				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-					condition.InstallationFailed, fmt.Sprintf("required package(s) not installed: %v", strings.Join(failed, ","))))
-			return false
-		}
-	} else if result.Status == dependency.ValidationResultStatusConflict {
-		var parts []string
-		for _, c := range result.Conflicts {
-			parts = append(parts, fmt.Sprintf("need version %v of %v but found %v", c.Required.Version, c.Actual.Name, c.Actual.Version))
-		}
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.InstallationFailed, fmt.Sprintf("conflicting dependencies: %v", strings.Join(parts, ","))))
-		return false
-	}
-
-	var ownedPackages []packagesv1alpha1.OwnedResourceRef
-	var waitingFor []string
-	// if all requirements fulfilled, status can be checked
-	for _, dep := range r.pi.Status.Manifest.Dependencies {
-		var requiredPkg packagesv1alpha1.Package
-		if err := r.Get(ctx, types.NamespacedName{
-			Namespace: r.pkg.Namespace,
-			Name:      dep.Name,
-		}, &requiredPkg); err != nil {
-			if apierrors.IsNotFound(err) {
-				waitingFor = append(waitingFor, dep.Name)
-			} else {
-				message := fmt.Sprintf("failed to get required package %v: %v", dep.Name, err)
-				r.setShouldUpdate(
-					conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions, condition.InstallationFailed, message))
-				return false
-			}
-		} else {
-			// if the required package already exists, we set the owner reference if
-			// * there already exists another package owner reference (i.e. it has been installed as dependency of another package)
-			// * but NOT if there exists no other package owner reference (i.e. it has been installed manually)
-			if ok, err := r.HasAnyOwnerOfType(r.pkg, &requiredPkg); err != nil || ok {
-				if err != nil {
-					log.Error(err, "Failed to check for owner references", "owner", r.pkg.Name, "owned", requiredPkg.Name)
-				}
-				if ok, err := r.HasOwner(r.pkg, &requiredPkg); err != nil || !ok {
-					log.Info("Updating existing required package with new owner reference", "owner", r.pkg.Name, "owned", requiredPkg.Name)
-					if err := r.SetOwner(r.pkg, &requiredPkg, owners.BlockOwnerDeletion); err != nil {
-						log.Error(err, "Failed to set owner reference", "owner", r.pkg.Name, "owned", requiredPkg.Name)
-						failed = append(failed, dep.Name)
-					}
-					if err := r.Update(ctx, &requiredPkg); err != nil {
-						log.Error(err, "Failed to updated required package", "owner", r.pkg.Name, "owned", requiredPkg.Name)
-						failed = append(failed, dep.Name)
-					}
-				}
-			}
-
-			if owned, err := ownerutils.ToOwnedResourceRef(r.Scheme, &requiredPkg); err != nil {
-				log.Error(err, "Failed to create OwnedResourceRef", "package", requiredPkg)
-				failed = append(failed, dep.Name)
-			} else {
-				ownedPackages = append(ownedPackages, owned)
-			}
-			if meta.IsStatusConditionTrue(requiredPkg.Status.Conditions, string(condition.Failed)) {
-				failed = append(failed, requiredPkg.Name)
-			} else if !meta.IsStatusConditionTrue(requiredPkg.Status.Conditions, string(condition.Ready)) {
-				waitingFor = append(waitingFor, requiredPkg.Name)
-			}
-		}
-	}
-	if len(failed) > 0 {
-		message := fmt.Sprintf("required package(s) not installed: %v", strings.Join(failed, ","))
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions, condition.InstallationFailed, message))
-		return false
-	}
-	if len(waitingFor) > 0 {
-		message := fmt.Sprintf("waiting for required package(s) %v", strings.Join(waitingFor, ","))
-		r.setShouldUpdate(
-			conditions.SetUnknown(ctx, &r.pkg.Status.Conditions, condition.Pending, message))
-		return false
-	}
-
-	ownerutils.Add(&r.currentOwnedPackages, ownedPackages...)
-
-	return true
-}
-
-func (r *PackageReconcilationContext) ensurePackageInfo(ctx context.Context) error {
-	packageInfo := packagesv1alpha1.PackageInfo{
-		ObjectMeta: metav1.ObjectMeta{Name: names.PackageInfoName(*r.pkg)},
-	}
-	log := ctrl.LoggerFrom(ctx).WithValues("PackageInfo", packageInfo.Name)
-
-	log.V(1).Info("ensuring PackageInfo")
-	result, err := controllerutil.CreateOrUpdate(ctx, r.Client, &packageInfo, func() error {
-		if err := r.SetOwner(r.pkg, &packageInfo, owners.BlockOwnerDeletion); err != nil {
-			return fmt.Errorf("unable to set ownerReference on PackageInfo: %w", err)
-		}
-		packageInfo.Spec = packagesv1alpha1.PackageInfoSpec{
-			Name:           r.pkg.Spec.PackageInfo.Name,
-			Version:        r.pkg.Spec.PackageInfo.Version,
-			RepositoryName: r.pkg.Spec.PackageInfo.RepositoryName,
-		}
-		return nil
-	})
-	if err != nil {
-		return fmt.Errorf("could not create or update PackageInfo: %w", err)
-	}
-
-	log.V(1).Info("ensured PackageInfo", "result", result)
-
-	// After CreateOrUpdate, PackageInfo does not always have correct TypeMeta.
-	// To fix this, we Get it again here.
-	// TODO: Find out why this is even needed.
-	if result == controllerutil.OperationResultCreated {
-		err := retry.OnError(retry.DefaultBackoff, apierrors.IsNotFound, func() error {
-			return r.Get(ctx, client.ObjectKeyFromObject(&packageInfo), &packageInfo)
-		})
-		if err != nil {
-			return err
-		}
-	}
-
-	if changed, err := ownerutils.AddOwnedResourceRef(r.Scheme, &r.pkg.Status.OwnedPackageInfos, &packageInfo); err != nil {
-		log.Error(err, "could not add PackageInfo to owned resources")
-	} else {
-		r.setShouldUpdate(changed)
-	}
-
-	r.pi = &packageInfo
-	return nil
-}
-
-func (r *PackageReconcilationContext) handleAdapterResults(ctx context.Context, results []result.ReconcileResult) bool {
-	var firstFailed *result.ReconcileResult
-	var firstWaiting *result.ReconcileResult
-	for i, result := range results {
-		if result.IsFailed() && firstFailed == nil {
-			firstFailed = &results[i]
-		} else if result.IsWaiting() && firstWaiting == nil {
-			firstWaiting = &results[i]
-		}
-	}
-
-	if firstFailed != nil {
-		r.setShouldUpdate(
-			conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-				condition.InstallationFailed, firstFailed.Message))
-		return false
-	} else if firstWaiting != nil {
-		r.setShouldUpdate(
-			conditions.SetUnknown(ctx, &r.pkg.Status.Conditions, condition.Pending, firstWaiting.Message))
-		return false
+	if bld, err := r.baseSetup(mgr, &v1alpha1.Package{}, r); err != nil {
+		return err
 	} else {
-		return true
+		return bld.Complete(r)
 	}
 }
 
-func (r *PackageReconcilationContext) afterSuccess(ctx context.Context, results []result.ReconcileResult) {
-	reason := condition.UpToDate
-	message := "PackageInfo has nothing to apply (no helm or kustomize manifest present)"
-	if len(results) > 0 {
-		messages := make([]string, 0, len(results))
-		for _, result := range results {
-			messages = append(messages, result.Message)
-		}
-		reason = condition.InstallationSucceeded
-		message = strings.Join(messages, "\n")
+func (r *PackageReconciler) ListPackages(ctx context.Context) ([]ctrlpkg.Package, error) {
+	var l v1alpha1.PackageList
+	if err := r.Client.List(ctx, &l); err != nil {
+		return nil, err
 	}
-
-	r.setShouldUpdate(
-		conditions.SetReady(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions, reason, message))
-	r.setShouldUpdate(r.pkg.Status.Version != r.pi.Status.Version)
-	r.pkg.Status.Version = r.pi.Status.Version
-	r.isSuccess = true
-}
-
-func (r *PackageReconcilationContext) finalize(ctx context.Context) (ctrl.Result, error) {
-	return requeue.Always(ctx, r.actualFinalize(ctx))
-}
-
-func (r *PackageReconcilationContext) finalizeNoRequeue(ctx context.Context) (ctrl.Result, error) {
-	return requeue.OnError(ctx, r.actualFinalize(ctx))
-}
-
-func (r *PackageReconcilationContext) finalizeWithError(ctx context.Context, err error) (ctrl.Result, error) {
-	return requeue.Always(ctx, multierr.Append(err, r.actualFinalize(ctx)))
-}
-
-func (r *PackageReconcilationContext) actualFinalize(ctx context.Context) error {
-	log := ctrl.LoggerFrom(ctx)
-
-	r.setShouldUpdate(ownerutils.Add(&r.pkg.Status.OwnedResources, r.currentOwnedResources...))
-	r.setShouldUpdate(ownerutils.Add(&r.pkg.Status.OwnedPackages, r.currentOwnedPackages...))
-
-	var errs error
-	if r.isSuccess {
-		if err := r.cleanup(ctx); err != nil {
-			r.setShouldUpdate(
-				conditions.SetFailed(ctx, r.EventRecorder, r.pkg, &r.pkg.Status.Conditions,
-					condition.InstallationFailed, fmt.Sprintf("cleanup failed: %v", err)))
-			errs = multierr.Append(errs, err)
-		}
-		log.V(1).Info("cleanup done")
-	}
-
-	if r.shouldUpdateStatus {
-		err := r.Status().Update(ctx, r.pkg)
-		if err != nil {
-			log.Error(err, "package status update failed")
-		} else {
-			log.Info("package status updated")
-		}
-		errs = multierr.Append(errs, err)
-	}
-	return errs
-}
-
-func (r *PackageReconcilationContext) cleanup(ctx context.Context) error {
-	return multierr.Combine(
-		r.pruneOwnedResources(ctx),
-		r.pruneOwnedPackageInfos(ctx),
-		r.pruneOwnedPackages(ctx),
-	)
-}
-
-func (r *PackageReconcilationContext) pruneOwnedResources(ctx context.Context) error {
-	log := ctrl.LoggerFrom(ctx)
-	var errs error
-	ownedResourcesCopy := r.pkg.Status.OwnedResources[:]
-OuterLoop:
-	for _, ref := range r.pkg.Status.OwnedResources {
-		for _, newRef := range r.currentOwnedResources {
-			if ownerutils.RefersToSameResource(ref, newRef) {
-				// ref is still an owned resource
-				continue OuterLoop
-			}
-		}
-		// ref is no longer an owned resource.
-		// check if it is managed by the operator and delete it if it is.
-		obj := ownerutils.OwnedResourceRefToObject(ref)
-		if err := r.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
-			if !apierrors.IsNotFound(err) {
-				multierr.AppendInto(&errs, fmt.Errorf("could get resource during pruning: %w", err))
-			}
-		} else if labels.IsManaged(obj) {
-			if err := r.Delete(ctx, obj); err != nil && !apierrors.IsNotFound(err) {
-				multierr.AppendInto(&errs, fmt.Errorf("could not prune resource: %w", err))
-			} else {
-				log.V(1).Info("pruned resource", "reference", ref)
-				r.setShouldUpdate(ownerutils.Remove(&ownedResourcesCopy, ref))
-			}
-		} else {
-			log.V(1).Info("skipped pruning unmanaged resource", "reference", ref)
-			r.setShouldUpdate(ownerutils.Remove(&ownedResourcesCopy, ref))
-		}
-	}
-	r.pkg.Status.OwnedResources = ownedResourcesCopy
-	return errs
-}
-
-func (r *PackageReconcilationContext) pruneOwnedPackageInfos(ctx context.Context) error {
-	log := ctrl.LoggerFrom(ctx)
-	currentRef, err := ownerutils.ToOwnedResourceRef(r.Scheme, r.pi)
-	if err != nil {
-		return err
-	}
-	var compositeErr error
-	for _, ref := range r.pkg.Status.OwnedPackageInfos {
-		if !ownerutils.RefersToSameResource(ref, currentRef) {
-			var packageInfo packagesv1alpha1.PackageInfo
-			key := client.ObjectKeyFromObject(ownerutils.OwnedResourceRefToObject(ref))
-			if err := r.Get(ctx, key, &packageInfo); apierrors.IsNotFound(err) {
-				log.Info("PackageInfo not found", "PackageInfo", ref.Name)
-			} else if err != nil {
-				compositeErr = multierr.Append(compositeErr, err)
-				continue
-			} else {
-				if owned, err := ownerutils.ObjHasOwner(&packageInfo, r.pkg); err != nil {
-					compositeErr = multierr.Append(compositeErr, err)
-					continue
-				} else if owned {
-					// Remove the owner reference for pkg
-					if err := controllerutil.RemoveOwnerReference(r.pkg, &packageInfo, r.Scheme); err != nil {
-						compositeErr = multierr.Append(compositeErr, err)
-						continue
-					}
-					if len(packageInfo.OwnerReferences) > 0 {
-						// If other owner references remain, update the PackageInfo with the owner reference removed
-						log.V(1).Info("updating old package info", "PackageInfo", packageInfo.Name)
-						if err := r.Update(ctx, &packageInfo); client.IgnoreNotFound(err) != nil {
-							compositeErr = multierr.Append(compositeErr, err)
-							continue
-						}
-					} else {
-						// If no other owner references remain, delete the PackageInfo
-						log.V(1).Info("deleting old package info", "PackageInfo", packageInfo.Name)
-						if err := r.Delete(ctx, &packageInfo); client.IgnoreNotFound(err) != nil {
-							compositeErr = multierr.Append(compositeErr, err)
-							continue
-						}
-					}
-				}
-			}
-
-			// Remove the PackageInfo from the owned PackageInfos field of pkg
-			ownerutils.RemoveOwnedResourceRef(&r.pkg.Status.OwnedPackageInfos, ref)
-			r.setShouldUpdate(true)
-		}
-	}
-	return compositeErr
-}
-
-func (r *PackageReconcilationContext) pruneOwnedPackages(ctx context.Context) error {
-	log := ctrl.LoggerFrom(ctx)
-	var errs error
-	ownedPackagesCopy := r.pkg.Status.OwnedPackages[:]
-OuterLoop:
-	for _, ref := range r.pkg.Status.OwnedPackages {
-		for _, newRef := range r.currentOwnedPackages {
-			if ownerutils.RefersToSameResource(ref, newRef) {
-				continue OuterLoop
-			}
-		}
-
-		var oldReqPkg v1alpha1.Package
-		if err := r.Get(ctx, types.NamespacedName{
-			Name:      ref.Name,
-			Namespace: ref.Namespace,
-		}, &oldReqPkg); err != nil {
-			if apierrors.IsNotFound(err) {
-				r.setShouldUpdate(ownerutils.Remove(&ownedPackagesCopy, ref))
-			} else {
-				log.Error(err, "Failed to get old required package", "oldPackage", ref.Name)
-			}
-		} else if owning, err := r.HasOwner(r.pkg, &oldReqPkg); err != nil {
-			log.Error(err, "Failed to check owner references of old required package", "oldPackage", ref.Name)
-		} else if owning {
-			if ref.MarkedForDeletion {
-				log.Info(fmt.Sprintf("reference from %v to %v marked for deletion will be removed", r.pkg.Name, ref.Name))
-				var cnt int
-				cnt, err = r.CountOwnersOfType(r.pkg, &oldReqPkg)
-				if err != nil {
-					log.Error(err, "Failed to check owner references of old required package", "oldPackage", ref.Name)
-					continue
-				}
-
-				if err := r.RemoveOwner(r.pkg, &oldReqPkg); err != nil {
-					log.Error(err, "Failed to remove owner reference", "oldPackage", ref.Name)
-				} else if err := r.Update(ctx, &oldReqPkg); err != nil {
-					log.Error(err, "Failed to update old package with removed owner reference", "oldPackage", ref.Name)
-				} else {
-					log.Info(fmt.Sprintf("Removed owner reference from %v to %v", r.pkg.Name, ref.Name))
-
-					if cnt == 1 {
-						// remove the old package if we were the only package owning it
-						deletePropagationForeground := metav1.DeletePropagationForeground
-						if err := r.Delete(ctx, ownerutils.OwnedResourceRefToObject(ref), &client.DeleteOptions{
-							PropagationPolicy: &deletePropagationForeground,
-						}); err != nil && !apierrors.IsNotFound(err) {
-							errs = multierr.Append(errs, fmt.Errorf("could not prune package: %w", err))
-						} else {
-							log.V(1).Info("pruned package", "reference", ref)
-						}
-					}
-				}
-			} else {
-				log.Info(fmt.Sprintf("marking for deletion: reference from %v to %v", r.pkg.Name, ref.Name))
-				r.setShouldUpdate(ownerutils.MarkForDeletion(&ownedPackagesCopy, ref))
-			}
-		} else {
-			r.setShouldUpdate(ownerutils.Remove(&ownedPackagesCopy, ref))
-		}
+	res := make([]ctrlpkg.Package, len(l.Items))
+	for i := range l.Items {
+		res[i] = &l.Items[i]
 	}
-	r.pkg.Status.OwnedPackages = ownedPackagesCopy
-	return errs
+	return res, nil
 }
diff --git a/internal/controller/watch/enqueue.go b/internal/controller/watch/enqueue.go
new file mode 100644
index 0000000..3f9a01a
--- /dev/null
+++ b/internal/controller/watch/enqueue.go
@@ -0,0 +1,42 @@
+package watch
+
+import (
+	"context"
+
+	ownerutils "github.com/glasskube/glasskube/internal/controller/owners/utils"
+	"k8s.io/apimachinery/pkg/runtime"
+	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/reconcile"
+)
+
+func EnqueueRequestsFromOwnedResource(
+	scheme *runtime.Scheme,
+	targetLister PackageLister,
+	ownedResourcesGetter ownedMapperFunc,
+) handler.EventHandler {
+	return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
+		log := ctrl.LoggerFrom(ctx)
+		objRef, err := ownerutils.ToOwnedResourceRef(scheme, obj)
+		if err != nil {
+			log.Error(err, "could not map object in event handler")
+			return nil
+		}
+		if pkgs, err := targetLister.ListPackages(ctx); err != nil {
+			log.Error(err, "could not list packages event handler")
+			return nil
+		} else {
+			var res []reconcile.Request
+			for _, pkg := range pkgs {
+				for _, ownedRef := range ownedResourcesGetter(pkg) {
+					if ownerutils.RefersToSameResource(ownedRef, objRef) {
+						res = append(res, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(pkg)})
+						break
+					}
+				}
+			}
+			return res
+		}
+	})
+}
diff --git a/internal/controller/watch/lister.go b/internal/controller/watch/lister.go
new file mode 100644
index 0000000..ae3207f
--- /dev/null
+++ b/internal/controller/watch/lister.go
@@ -0,0 +1,11 @@
+package watch
+
+import (
+	"context"
+
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
+)
+
+type PackageLister interface {
+	ListPackages(ctx context.Context) ([]ctrlpkg.Package, error)
+}
diff --git a/internal/controller/watch/mapper.go b/internal/controller/watch/mapper.go
new file mode 100644
index 0000000..e2d166e
--- /dev/null
+++ b/internal/controller/watch/mapper.go
@@ -0,0 +1,19 @@
+package watch
+
+import (
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
+)
+
+type ownedMapperFunc func(pkg ctrlpkg.Package) []v1alpha1.OwnedResourceRef
+
+var _ ownedMapperFunc = OwnedPackageInfos
+var _ ownedMapperFunc = OwnedPackages
+
+func OwnedPackageInfos(pkg ctrlpkg.Package) []v1alpha1.OwnedResourceRef {
+	return pkg.GetStatus().OwnedPackageInfos
+}
+
+func OwnedPackages(pkg ctrlpkg.Package) []v1alpha1.OwnedResourceRef {
+	return pkg.GetStatus().OwnedPackages
+}
diff --git a/internal/dependency/graph/graph.go b/internal/dependency/graph/graph.go
index 8fd301b..0fb1e20 100644
--- a/internal/dependency/graph/graph.go
+++ b/internal/dependency/graph/graph.go
@@ -9,14 +9,52 @@ import (
 	"go.uber.org/multierr"
 )
 
+type vertexMap map[string]*vertex
+
+func (m vertexMap) vertex(key string) *vertex {
+	if v, ok := m[key]; ok {
+		return v
+	} else {
+		return m.createVertex(key)
+	}
+}
+
+func (m vertexMap) createVertex(key string) *vertex {
+	n := &vertex{edges: map[string]*edge{}}
+	m[key] = n
+	return n
+}
+
+func (m *vertexMap) edge(from *vertex, to string, constraint *semver.Constraints) {
+	from.edges[to] = &edge{
+		vertex:     m.vertex(to),
+		constraint: constraint,
+	}
+}
+
+func (oldMap vertexMap) deepCopy() vertexMap {
+	newMap := vertexMap{}
+	for key, vertex := range oldMap {
+		newVertex := newMap.vertex(key)
+		newVertex.version = vertex.version
+		newVertex.manual = vertex.manual
+		for edgeName, edge := range vertex.edges {
+			newMap.edge(newVertex, edgeName, edge.constraint)
+		}
+	}
+	return newMap
+}
+
 type DependencyGraph struct {
-	vertices map[string]*vertex
+	clusterVertices    vertexMap
+	namespacedVertices vertexMap
 }
 
 type vertex struct {
-	version *semver.Version
-	manual  bool
-	edges   map[string]*edge
+	reference *string
+	version   *semver.Version
+	manual    bool
+	edges     map[string]*edge
 }
 
 type edge struct {
@@ -25,21 +63,33 @@ type edge struct {
 }
 
 func NewGraph() *DependencyGraph {
-	return &DependencyGraph{vertices: map[string]*vertex{}}
+	return &DependencyGraph{
+		clusterVertices:    vertexMap{},
+		namespacedVertices: vertexMap{},
+	}
 }
 
-// Add simulates installing or updating a package by
+// AddCluster simulates installing or updating a ClusterPackage by
 // 1. Creating a vertex if necessary
 // 2. Setting its version and
 // 3. Updating the outgoing edges of the vertex to match the manifests dependencies declaration
-func (g *DependencyGraph) Add(name, version string, dependencies []v1alpha1.Dependency, manual bool) error {
-	_, err := g.add(name, version, dependencies, manual)
+func (g *DependencyGraph) AddCluster(name, version string, dependencies []v1alpha1.Dependency, manual bool) error {
+	_, err := g.add(g.clusterVertex(name), version, dependencies, manual)
 	return err
 }
 
+func (g *DependencyGraph) AddNamespaced(reference, name, version string, dependencies []v1alpha1.Dependency) error {
+	if vertex, err := g.add(g.namespacedVertex(name, version), version, dependencies, true); err != nil {
+		return err
+	} else if reference != "" {
+		vertex.reference = &reference
+	}
+	return nil
+}
+
 // Manual returns whether a package has been manually added by a user
 func (g *DependencyGraph) Manual(name string) bool {
-	if vertex, ok := g.vertices[name]; ok {
+	if vertex, ok := g.clusterVertices[name]; ok {
 		return vertex.manual
 	} else {
 		return false
@@ -48,7 +98,7 @@ func (g *DependencyGraph) Manual(name string) bool {
 
 // Version returns the installed version of a package or nil if that package is not installed
 func (g *DependencyGraph) Version(of string) *semver.Version {
-	if vertex, ok := g.vertices[of]; ok {
+	if vertex, ok := g.clusterVertices[of]; ok {
 		return vertex.version
 	} else {
 		return nil
@@ -57,7 +107,7 @@ func (g *DependencyGraph) Version(of string) *semver.Version {
 
 // Dependencies returns the names of packages that this package depends on
 func (g *DependencyGraph) Dependencies(of string) []string {
-	if vertex, ok := g.vertices[of]; ok {
+	if vertex, ok := g.clusterVertices[of]; ok {
 		dependencies := make([]string, len(vertex.edges))
 		i := 0
 		for dep := range vertex.edges {
@@ -73,18 +123,28 @@ func (g *DependencyGraph) Dependencies(of string) []string {
 // Dependants returns the names of packages that depend on this package
 func (g *DependencyGraph) Dependants(of string) []string {
 	var dependants []string
-	for name, vertex := range g.vertices {
+	for name, vertex := range g.clusterVertices {
 		if _, ok := vertex.edges[of]; vertex.version != nil && ok {
 			dependants = append(dependants, name)
 		}
 	}
+	for _, vertex := range g.namespacedVertices {
+		if _, ok := vertex.edges[of]; vertex.version != nil && ok {
+			dependants = append(dependants, *vertex.reference)
+		}
+	}
 	return dependants
 }
 
 // Constraints returns all constraints of dependants of this package
 func (g *DependencyGraph) Constraints(of string) []*semver.Constraints {
 	var constraints []*semver.Constraints
-	for _, vertex := range g.vertices {
+	for _, vertex := range g.clusterVertices {
+		if edge, ok := vertex.edges[of]; ok && vertex.version != nil && edge.constraint != nil {
+			constraints = append(constraints, edge.constraint)
+		}
+	}
+	for _, vertex := range g.namespacedVertices {
 		if edge, ok := vertex.edges[of]; ok && vertex.version != nil && edge.constraint != nil {
 			constraints = append(constraints, edge.constraint)
 		}
@@ -115,16 +175,10 @@ outer:
 
 // DeepCopy returns an exact copy of this graph
 func (oldGraph *DependencyGraph) DeepCopy() *DependencyGraph {
-	newGraph := NewGraph()
-	for vertexName, vertex := range oldGraph.vertices {
-		newVertex := newGraph.vertex(vertexName)
-		newVertex.version = vertex.version
-		newVertex.manual = vertex.manual
-		for edgeName, edge := range vertex.edges {
-			newGraph.edge(vertexName, edgeName, edge.constraint)
-		}
+	return &DependencyGraph{
+		clusterVertices:    oldGraph.clusterVertices.deepCopy(),
+		namespacedVertices: oldGraph.namespacedVertices.deepCopy(),
 	}
-	return newGraph
 }
 
 // Delete simulates uninstalling a package.
@@ -133,7 +187,7 @@ func (oldGraph *DependencyGraph) DeepCopy() *DependencyGraph {
 // packages and needs to be kept for validation! Instead, its version is unset and its dependencies
 // are cleared.
 func (g *DependencyGraph) Delete(name string) bool {
-	return g.delete(name)
+	return g.delete(g.clusterVertex(name))
 }
 
 // Prune deletes all vertices for which all of the following applies:
@@ -144,8 +198,8 @@ func (g *DependencyGraph) Prune() []string {
 	var removed []string
 	for !stable {
 		stable = true
-		for name, vertex := range g.vertices {
-			if !vertex.manual && len(g.Dependants(name)) == 0 && g.delete(name) {
+		for name, vertex := range g.clusterVertices {
+			if !vertex.manual && len(g.Dependants(name)) == 0 && g.delete(g.clusterVertex(name)) {
 				stable = false
 				removed = append(removed, name)
 			}
@@ -155,7 +209,7 @@ func (g *DependencyGraph) Prune() []string {
 }
 
 func (g *DependencyGraph) DeleteAndPrune(name string) []string {
-	if g.delete(name) {
+	if g.delete(g.clusterVertex(name)) {
 		return append([]string{name}, g.Prune()...)
 	}
 	return nil
@@ -171,48 +225,48 @@ func (g *DependencyGraph) ValidateDelete(name string) ([]string, error) {
 // 2. There are no violated version constraints
 func (g *DependencyGraph) Validate() error {
 	var err error
-	for name, vertex := range g.vertices {
+	for name, vertex := range g.clusterVertices {
 		for dep, edge := range vertex.edges {
 			if edge.vertex.version == nil {
-				multierr.AppendInto(&err, ErrDependency(name, dep, ErrNotInstalled(dep)))
+				multierr.AppendInto(&err, ErrDependency(name, dep,
+					ErrNotInstalled(dep)))
 			} else if edge.constraint != nil {
 				if err1 := isemver.ValidateVersionConstraint(edge.vertex.version, edge.constraint); err1 != nil {
-					multierr.AppendInto(&err, ErrDependency(name, dep, ErrConstraint(dep, edge.vertex.version, edge.constraint, err1)))
+					multierr.AppendInto(&err, ErrDependency(name, dep,
+						ErrConstraint(dep, edge.vertex.version, edge.constraint, err1)))
 				}
 			}
 		}
 	}
-	return err
-}
-
-func (g *DependencyGraph) vertex(name string) *vertex {
-	if n, ok := g.vertices[name]; ok {
-		return n
-	} else {
-		return g.createVertex(name, nil)
+	for _, vertex := range g.namespacedVertices {
+		for dep, edge := range vertex.edges {
+			if edge.vertex.version == nil {
+				multierr.AppendInto(&err, ErrDependency(*vertex.reference, dep,
+					ErrNotInstalled(dep)))
+			} else if edge.constraint != nil {
+				if err1 := isemver.ValidateVersionConstraint(edge.vertex.version, edge.constraint); err1 != nil {
+					multierr.AppendInto(&err, ErrDependency(*vertex.reference, dep,
+						ErrConstraint(dep, edge.vertex.version, edge.constraint, err1)))
+				}
+			}
+		}
 	}
+	return err
 }
 
-func (g *DependencyGraph) createVertex(name string, version *semver.Version) *vertex {
-	n := &vertex{
-		version: version,
-		edges:   map[string]*edge{},
-	}
-	g.vertices[name] = n
-	return n
+func (g *DependencyGraph) clusterVertex(name string) *vertex {
+	return g.clusterVertices.vertex(name)
 }
 
-func (g *DependencyGraph) edge(from string, to string, constraint *semver.Constraints) {
-	g.vertex(from).edges[to] = &edge{
-		vertex:     g.vertex(to),
-		constraint: constraint,
-	}
+func (g *DependencyGraph) namespacedVertex(name, version string) *vertex {
+	return g.namespacedVertices.vertex(fmt.Sprintf("%v@%v", name, version))
 }
 
-func (g *DependencyGraph) add(name, version string, dependencies []v1alpha1.Dependency, manual bool) (*vertex, error) {
+func (g *DependencyGraph) add(
+	vertex *vertex, version string, dependencies []v1alpha1.Dependency, manual bool) (*vertex, error) {
 	if version == "" {
-		g.delete(name)
-		return g.vertex(name), nil
+		g.delete(vertex)
+		return vertex, nil
 	}
 
 	parsedVersion, err := semver.NewVersion(version)
@@ -220,7 +274,6 @@ func (g *DependencyGraph) add(name, version string, dependencies []v1alpha1.Depe
 		return nil, err
 	}
 
-	vertex := g.vertex(name)
 	vertex.version = parsedVersion
 	vertex.manual = manual
 	vertex.edges = map[string]*edge{}
@@ -234,14 +287,13 @@ func (g *DependencyGraph) add(name, version string, dependencies []v1alpha1.Depe
 				constraint = c
 			}
 		}
-		g.edge(name, dep.Name, constraint)
+		g.clusterVertices.edge(vertex, dep.Name, constraint)
 	}
 
 	return vertex, nil
 }
 
-func (g *DependencyGraph) delete(name string) bool {
-	vertex := g.vertex(name)
+func (g *DependencyGraph) delete(vertex *vertex) bool {
 	deleted := vertex.version != nil
 	vertex.version = nil
 	vertex.manual = false
diff --git a/internal/dependency/manager.go b/internal/dependency/manager.go
index 53e0617..b290ebb 100644
--- a/internal/dependency/manager.go
+++ b/internal/dependency/manager.go
@@ -3,9 +3,11 @@ package dependency
 import (
 	"context"
 	"errors"
+	"fmt"
 	"slices"
 	"strings"
 
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/names"
 	"go.uber.org/multierr"
 
@@ -87,15 +89,28 @@ func (dm *DependendcyManager) Validate(
 
 // NewGraph constructs a DependencyGraph from all packages returned by clientAdapter.ListPackages
 func (dm *DependendcyManager) NewGraph(ctx context.Context) (*graph.DependencyGraph, error) {
-	pkgs, err := dm.pkgClient.ListPackages(ctx)
-	if err != nil {
+	var allPkgs []ctrlpkg.Package
+	if pkgs, err := dm.pkgClient.ListClusterPackages(ctx); err != nil {
 		return nil, err
+	} else {
+		for i := range pkgs.Items {
+			allPkgs = append(allPkgs, &pkgs.Items[i])
+		}
+	}
+
+	if pkgs, err := dm.pkgClient.ListPackages(ctx, ""); err != nil {
+		return nil, err
+	} else {
+		for i := range pkgs.Items {
+			allPkgs = append(allPkgs, &pkgs.Items[i])
+		}
 	}
+
 	g := graph.NewGraph()
-	for _, pkg := range pkgs.Items {
+	for _, pkg := range allPkgs {
 		var deps []v1alpha1.Dependency
-		installedVersion := pkg.Spec.PackageInfo.Version
-		if !pkg.DeletionTimestamp.IsZero() {
+		installedVersion := pkg.GetSpec().PackageInfo.Version
+		if !pkg.GetDeletionTimestamp().IsZero() {
 			// A package that is currently being deleted is added to the graph, but in a state representing
 			// "not installed"
 			installedVersion = ""
@@ -104,8 +119,24 @@ func (dm *DependendcyManager) NewGraph(ctx context.Context) (*graph.DependencyGr
 		} else if pi.Status.Manifest != nil {
 			deps = pi.Status.Manifest.Dependencies
 		}
-		if err := g.Add(pkg.Name, installedVersion, deps, len(pkg.OwnerReferences) == 0); err != nil {
-			return nil, err
+		if pkg.IsNamespaceScoped() {
+			if err := g.AddNamespaced(
+				fmt.Sprintf("%v.%v", pkg.GetName(), pkg.GetNamespace()),
+				pkg.GetSpec().PackageInfo.Name,
+				installedVersion,
+				deps,
+			); err != nil {
+				return nil, err
+			}
+		} else {
+			if err := g.AddCluster(
+				pkg.GetSpec().PackageInfo.Name,
+				installedVersion,
+				deps,
+				len(pkg.GetOwnerReferences()) == 0,
+			); err != nil {
+				return nil, err
+			}
 		}
 	}
 	return g, nil
@@ -116,7 +147,7 @@ func (dm *DependendcyManager) add(
 	manifest v1alpha1.PackageManifest,
 	version string,
 ) error {
-	return g.Add(manifest.Name, version, manifest.Dependencies, g.Manual(manifest.Name))
+	return g.AddCluster(manifest.Name, version, manifest.Dependencies, g.Manual(manifest.Name))
 }
 
 // addDependencies adds the highest possible version of every uninstalled dependency and installs all transitive
diff --git a/internal/manifest/adapter.go b/internal/manifest/adapter.go
index a1908ee..eddcd8b 100644
--- a/internal/manifest/adapter.go
+++ b/internal/manifest/adapter.go
@@ -4,6 +4,7 @@ import (
 	"context"
 
 	packagesv1alpha1 "github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/manifest/result"
 	"github.com/glasskube/glasskube/internal/manifestvalues"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -14,7 +15,7 @@ import (
 type ManifestAdapter interface {
 	ControllerInit(builder *builder.Builder, client client.Client, scheme *runtime.Scheme) error
 	Reconcile(ctx context.Context,
-		pkg *packagesv1alpha1.Package,
+		pkg ctrlpkg.Package,
 		manifest *packagesv1alpha1.PackageManifest,
 		patches manifestvalues.TargetPatches,
 	) (*result.ReconcileResult, error)
diff --git a/internal/manifest/helm/flux/adapter.go b/internal/manifest/helm/flux/adapter.go
index d3563a0..01634f0 100644
--- a/internal/manifest/helm/flux/adapter.go
+++ b/internal/manifest/helm/flux/adapter.go
@@ -9,6 +9,7 @@ import (
 	helmv1beta2 "github.com/fluxcd/helm-controller/api/v2beta2"
 	sourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2"
 	packagesv1alpha1 "github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/controller/labels"
 	"github.com/glasskube/glasskube/internal/controller/owners"
 	"github.com/glasskube/glasskube/internal/controller/owners/utils"
@@ -54,20 +55,22 @@ func (a *FluxHelmAdapter) ControllerInit(buildr *builder.Builder, client client.
 
 func (a *FluxHelmAdapter) Reconcile(
 	ctx context.Context,
-	pkg *packagesv1alpha1.Package,
+	pkg ctrlpkg.Package,
 	manifest *packagesv1alpha1.PackageManifest,
 	patches manifestvalues.TargetPatches,
 ) (*result.ReconcileResult, error) {
 	log := ctrl.LoggerFrom(ctx)
 	var ownedResources []packagesv1alpha1.OwnedResourceRef
-	if namespace, err := a.ensureNamespace(ctx, pkg, manifest); err != nil {
-		return nil, err
-	} else {
-		if _, err := utils.AddOwnedResourceRef(a.Scheme(), &ownedResources, namespace); err != nil {
-			log.Error(err, "could not add Namespace to ownedResources")
-		}
-		if namespace.Status.Phase == corev1.NamespaceTerminating {
-			return result.Waiting("Namespace is still terminating", ownedResources), nil
+	if !pkg.IsNamespaceScoped() {
+		if namespace, err := a.ensureNamespace(ctx, pkg, manifest); err != nil {
+			return nil, err
+		} else {
+			if _, err := utils.AddOwnedResourceRef(a.Scheme(), &ownedResources, namespace); err != nil {
+				log.Error(err, "could not add Namespace to ownedResources")
+			}
+			if namespace.Status.Phase == corev1.NamespaceTerminating {
+				return result.Waiting("Namespace is still terminating", ownedResources), nil
+			}
 		}
 	}
 	if helmRepository, err := a.ensureHelmRepository(ctx, pkg, manifest); err != nil {
@@ -89,7 +92,7 @@ func (a *FluxHelmAdapter) Reconcile(
 
 func (a *FluxHelmAdapter) ensureNamespace(
 	ctx context.Context,
-	pkg *packagesv1alpha1.Package,
+	pkg ctrlpkg.Package,
 	manifest *packagesv1alpha1.PackageManifest,
 ) (*corev1.Namespace, error) {
 	namespace := corev1.Namespace{
@@ -115,13 +118,19 @@ func (a *FluxHelmAdapter) ensureNamespace(
 
 func (a *FluxHelmAdapter) ensureHelmRepository(
 	ctx context.Context,
-	pkg *packagesv1alpha1.Package,
+	pkg ctrlpkg.Package,
 	manifest *packagesv1alpha1.PackageManifest,
 ) (*sourcev1beta2.HelmRepository, error) {
+	var namespace string
+	if pkg.IsNamespaceScoped() {
+		namespace = pkg.GetNamespace()
+	} else {
+		namespace = manifest.DefaultNamespace
+	}
 	helmRepository := sourcev1beta2.HelmRepository{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      manifest.Name,
-			Namespace: manifest.DefaultNamespace,
+			Name:      helmResourceName(pkg, manifest),
+			Namespace: namespace,
 		},
 	}
 	log := ctrl.LoggerFrom(ctx).WithValues("HelmRepository", helmRepository.Name)
@@ -141,14 +150,20 @@ func (a *FluxHelmAdapter) ensureHelmRepository(
 
 func (a *FluxHelmAdapter) ensureHelmRelease(
 	ctx context.Context,
-	pkg *packagesv1alpha1.Package,
+	pkg ctrlpkg.Package,
 	manifest *packagesv1alpha1.PackageManifest,
 	patches manifestvalues.TargetPatches,
 ) (*helmv1beta2.HelmRelease, error) {
+	var namespace string
+	if pkg.IsNamespaceScoped() {
+		namespace = pkg.GetNamespace()
+	} else {
+		namespace = manifest.DefaultNamespace
+	}
 	helmRelease := helmv1beta2.HelmRelease{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      manifest.Name,
-			Namespace: manifest.DefaultNamespace,
+			Name:      helmResourceName(pkg, manifest),
+			Namespace: namespace,
 		},
 	}
 	log := ctrl.LoggerFrom(ctx).WithValues("HelmRelease", helmRelease.Name)
@@ -156,7 +171,7 @@ func (a *FluxHelmAdapter) ensureHelmRelease(
 		helmRelease.Spec.Chart.Spec.Chart = manifest.Helm.ChartName
 		helmRelease.Spec.Chart.Spec.Version = manifest.Helm.ChartVersion
 		helmRelease.Spec.Chart.Spec.SourceRef.Kind = "HelmRepository"
-		helmRelease.Spec.Chart.Spec.SourceRef.Name = manifest.Name
+		helmRelease.Spec.Chart.Spec.SourceRef.Name = helmResourceName(pkg, manifest)
 		if manifest.Helm.Values != nil {
 			helmRelease.Spec.Values = &apiextensionsv1.JSON{Raw: manifest.Helm.Values.Raw[:]}
 		} else {
@@ -201,3 +216,7 @@ func extractResult(
 		return result.Waiting("Waiting for HelmRelease reconciliation", ownedResources)
 	}
 }
+
+func helmResourceName(pkg ctrlpkg.Package, manifest *packagesv1alpha1.PackageManifest) string {
+	return strings.Join([]string{pkg.GetName(), manifest.Name}, "-")
+}
diff --git a/internal/manifest/plain/adapter.go b/internal/manifest/plain/adapter.go
index 8e210b3..47d3e7b 100644
--- a/internal/manifest/plain/adapter.go
+++ b/internal/manifest/plain/adapter.go
@@ -6,6 +6,7 @@ import (
 
 	packagesv1alpha1 "github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/clientutils"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/controller/owners"
 	ownerutils "github.com/glasskube/glasskube/internal/controller/owners/utils"
 	"github.com/glasskube/glasskube/internal/manifest"
@@ -49,13 +50,13 @@ func (a *Adapter) ControllerInit(builder *builder.Builder, client client.Client,
 // Reconcile implements manifest.ManifestAdapter.
 func (a *Adapter) Reconcile(
 	ctx context.Context,
-	pkg *packagesv1alpha1.Package,
+	pkg ctrlpkg.Package,
 	manifest *packagesv1alpha1.PackageManifest,
 	patches manifestvalues.TargetPatches,
 ) (*result.ReconcileResult, error) {
 	var allOwned []packagesv1alpha1.OwnedResourceRef
 	for _, m := range manifest.Manifests {
-		if owned, err := a.reconcilePlainManifest(ctx, *pkg, *manifest, m, patches); err != nil {
+		if owned, err := a.reconcilePlainManifest(ctx, pkg, *manifest, m, patches); err != nil {
 			return nil, err
 		} else {
 			allOwned = append(allOwned, owned...)
@@ -66,7 +67,7 @@ func (a *Adapter) Reconcile(
 
 func (r *Adapter) reconcilePlainManifest(
 	ctx context.Context,
-	pkg packagesv1alpha1.Package,
+	pkg ctrlpkg.Package,
 	pkgManifest packagesv1alpha1.PackageManifest,
 	manifest packagesv1alpha1.PlainManifest,
 	patches manifestvalues.TargetPatches,
@@ -85,48 +86,58 @@ func (r *Adapter) reconcilePlainManifest(
 
 	log.V(1).Info("fetched "+manifest.Url, "objectCount", len(objectsToApply))
 
-	// Determine the name of the default namespace. The more specific name takes precedence
-	defaultNamespaceName := pkgManifest.DefaultNamespace
-	if len(manifest.DefaultNamespace) > 0 {
-		defaultNamespaceName = manifest.DefaultNamespace
-	}
-
-	if len(defaultNamespaceName) > 0 {
-		defaultNamespaceRequired := false
-		defaultNamespaceInList := false
-
-		// Determine if the default namespace is needed (at least one resource would be created in the default namespace)
-		// and set the namespace property to the default namespace for all objects that are known to be namespaced and do
-		// not have an explicit namespace.
-		// Also, determine if a namespace resource with the default namespace name already exists in the list.
+	if pkg.IsNamespaceScoped() {
 		for _, obj := range objectsToApply {
-			if obj.GetObjectKind().GroupVersionKind() == r.namespaceGVK && obj.GetName() == defaultNamespaceName {
-				defaultNamespaceInList = true
-			} else {
-				if isNamespaced, err := r.IsObjectNamespaced(obj); err != nil {
-					// It can not be determined whether this obj kind is namespaced.
-					// This can happen if the obj kind is a CRD or some other type that the client does not know.
-					// TODO: Should we assume it is namespaced or not? Or just throw an error?
-					return nil, err
-				} else if isNamespaced {
-					if obj.GetNamespace() == "" {
-						obj.SetNamespace(defaultNamespaceName)
-					}
+			if isNamespaced, err := r.IsObjectNamespaced(obj); err != nil {
+				return nil, err
+			} else if isNamespaced {
+				obj.SetNamespace(pkg.GetNamespace())
+			}
+		}
+	} else {
+		// Determine the name of the default namespace. The more specific name takes precedence
+		defaultNamespaceName := pkgManifest.DefaultNamespace
+		if len(manifest.DefaultNamespace) > 0 {
+			defaultNamespaceName = manifest.DefaultNamespace
+		}
 
-					if obj.GetNamespace() == defaultNamespaceName {
-						defaultNamespaceRequired = true
+		if len(defaultNamespaceName) > 0 {
+			defaultNamespaceRequired := false
+			defaultNamespaceInList := false
+
+			// Determine if the default namespace is needed (at least one resource would be created in the default namespace)
+			// and set the namespace property to the default namespace for all objects that are known to be namespaced and do
+			// not have an explicit namespace.
+			// Also, determine if a namespace resource with the default namespace name already exists in the list.
+			for _, obj := range objectsToApply {
+				if obj.GetObjectKind().GroupVersionKind() == r.namespaceGVK && obj.GetName() == defaultNamespaceName {
+					defaultNamespaceInList = true
+				} else {
+					if isNamespaced, err := r.IsObjectNamespaced(obj); err != nil {
+						// It can not be determined whether this obj kind is namespaced.
+						// This can happen if the obj kind is a CRD or some other type that the client does not know.
+						// TODO: Should we assume it is namespaced or not? Or just throw an error?
+						return nil, err
+					} else if isNamespaced {
+						if obj.GetNamespace() == "" {
+							obj.SetNamespace(defaultNamespaceName)
+						}
+
+						if obj.GetNamespace() == defaultNamespaceName {
+							defaultNamespaceRequired = true
+						}
 					}
 				}
 			}
-		}
 
-		if defaultNamespaceRequired && !defaultNamespaceInList {
-			defaultNamespace := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: defaultNamespaceName}}
-			// It is necessary to set the GVK manually on this namespace.
-			// This could be because we use SSA here.
-			// TODO: Find out why!
-			defaultNamespace.SetGroupVersionKind(r.namespaceGVK)
-			objectsToApply = append([]client.Object{&defaultNamespace}, objectsToApply...)
+			if defaultNamespaceRequired && !defaultNamespaceInList {
+				defaultNamespace := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: defaultNamespaceName}}
+				// It is necessary to set the GVK manually on this namespace.
+				// This could be because we use SSA here.
+				// TODO: Find out why!
+				defaultNamespace.SetGroupVersionKind(r.namespaceGVK)
+				objectsToApply = append([]client.Object{&defaultNamespace}, objectsToApply...)
+			}
 		}
 	}
 
@@ -134,7 +145,7 @@ func (r *Adapter) reconcilePlainManifest(
 
 	// Apply any modifications before changing anything on the cluster
 	for _, obj := range objectsToApply {
-		if err := r.SetOwnerIfManagedOrNotExists(r.Client, ctx, &pkg, obj); err != nil {
+		if err := r.SetOwnerIfManagedOrNotExists(r.Client, ctx, pkg, obj); err != nil {
 			return nil, err
 		}
 		if err := patches.ApplyToResource(obj); err != nil {
diff --git a/internal/manifestvalues/resolver.go b/internal/manifestvalues/resolver.go
index 510e5f3..1c34a8a 100644
--- a/internal/manifestvalues/resolver.go
+++ b/internal/manifestvalues/resolver.go
@@ -85,7 +85,7 @@ func (r *Resolver) resolveSecretRef(ctx context.Context, ref v1alpha1.ObjectKeyV
 }
 
 func (r *Resolver) resolvePackageRef(ctx context.Context, ref v1alpha1.PackageValueSource) (string, error) {
-	if pkg, err := r.pkg.GetPackage(ctx, ref.Name); err != nil {
+	if pkg, err := r.pkg.GetClusterPackage(ctx, ref.Name); err != nil {
 		return "", NewPackageRefError(ref, err)
 	} else if value, ok := pkg.Spec.Values[ref.Value]; !ok {
 		return "", NewPackageRefError(ref, NewKeyError(ref.Value))
diff --git a/internal/manifestvalues/validate.go b/internal/manifestvalues/validate.go
index 1570bd6..f75c136 100644
--- a/internal/manifestvalues/validate.go
+++ b/internal/manifestvalues/validate.go
@@ -1,10 +1,12 @@
 package manifestvalues
 
 import (
+	"fmt"
 	"regexp"
 	"strconv"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"go.uber.org/multierr"
 )
 
@@ -13,14 +15,14 @@ type validateFn func(def v1alpha1.ValueDefinition, value string) error
 var (
 	validateMinLength validateFn = func(def v1alpha1.ValueDefinition, value string) error {
 		if def.Constraints.MinLength != nil && len(value) < *def.Constraints.MinLength {
-			return ErrConstraintMinLength
+			return fmt.Errorf("%w: %v", ErrConstraintMinLength, *def.Constraints.MinLength)
 		}
 		return nil
 	}
 
 	validateMaxLength validateFn = func(def v1alpha1.ValueDefinition, value string) error {
 		if def.Constraints.MaxLength != nil && len(value) > *def.Constraints.MaxLength {
-			return ErrConstraintMaxLength
+			return fmt.Errorf("%w: %v", ErrConstraintMaxLength, *def.Constraints.MaxLength)
 		}
 		return nil
 	}
@@ -44,7 +46,7 @@ var (
 			if i, err := strconv.Atoi(value); err != nil {
 				return err
 			} else if i < *def.Constraints.Min {
-				return ErrConstraintMin
+				return fmt.Errorf("%w: %v", ErrConstraintMin, *def.Constraints.Min)
 			}
 		}
 		return nil
@@ -55,7 +57,7 @@ var (
 			if i, err := strconv.Atoi(value); err != nil {
 				return err
 			} else if i > *def.Constraints.Max {
-				return ErrConstraintMax
+				return fmt.Errorf("%w: %v", ErrConstraintMax, *def.Constraints.Max)
 			}
 		}
 		return nil
@@ -161,9 +163,9 @@ func targetsForResolvedValues(values map[string]string) map[string]validationTar
 	return result
 }
 
-func targetsForPackage(pkg *v1alpha1.Package) map[string]validationTarget {
+func targetsForPackage(pkg ctrlpkg.Package) map[string]validationTarget {
 	result := make(map[string]validationTarget)
-	for name, value := range pkg.Spec.Values {
+	for name, value := range pkg.GetSpec().Values {
 		if value.Value != nil {
 			result[name] = acutalValue(*value.Value)
 		} else {
@@ -182,6 +184,6 @@ func ValidateResolvedValues(manifest v1alpha1.PackageManifest, values map[string
 // Reference values are **not resolved**, so constraint validation is skipped for
 // these values. If instead you want to validate **all** values, please resolve all
 // references first, using a Resolver instance, and then use ValidateResolvedValues.
-func ValidatePackage(manifest v1alpha1.PackageManifest, pkg *v1alpha1.Package) error {
+func ValidatePackage(manifest v1alpha1.PackageManifest, pkg ctrlpkg.Package) error {
 	return validate(manifest, targetsForPackage(pkg))
 }
diff --git a/internal/names/package_info.go b/internal/names/package_info.go
index c8a7953..333129b 100644
--- a/internal/names/package_info.go
+++ b/internal/names/package_info.go
@@ -1,11 +1,16 @@
 package names
 
 import (
-	"fmt"
+	"strings"
 
-	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 )
 
-func PackageInfoName(pkg v1alpha1.Package) string {
-	return escapeResourceName(fmt.Sprintf("%v--%v", pkg.Spec.PackageInfo.Name, pkg.Spec.PackageInfo.Version))
+func PackageInfoName(pkg ctrlpkg.Package) string {
+	spec := pkg.GetSpec()
+	parts := []string{spec.PackageInfo.Name, spec.PackageInfo.Version}
+	if spec.PackageInfo.RepositoryName != "" {
+		parts = append([]string{spec.PackageInfo.RepositoryName}, parts...)
+	}
+	return escapeResourceName(strings.Join(parts, "--"))
 }
diff --git a/internal/repo/client/clientset.go b/internal/repo/client/clientset.go
index a9ecb60..b385697 100644
--- a/internal/repo/client/clientset.go
+++ b/internal/repo/client/clientset.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/adapter"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	corev1 "k8s.io/api/core/v1"
 )
 
@@ -43,8 +44,8 @@ func NewClientsetWithMaxCacheAge(pkgClient adapter.PackageClientAdapter, k8sClie
 }
 
 // ForPackage implements RepoClientset.
-func (d *defaultClientset) ForPackage(pkg v1alpha1.Package) RepoClient {
-	return d.ForRepoWithName(pkg.Spec.PackageInfo.RepositoryName)
+func (d *defaultClientset) ForPackage(pkg ctrlpkg.Package) RepoClient {
+	return d.ForRepoWithName(pkg.GetSpec().PackageInfo.RepositoryName)
 }
 
 // ForRepo implements RepoClientset.
diff --git a/internal/repo/client/fake/fakeclient.go b/internal/repo/client/fake/fakeclient.go
index 0bb99a1..10723a4 100644
--- a/internal/repo/client/fake/fakeclient.go
+++ b/internal/repo/client/fake/fakeclient.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/repo/client"
 	"github.com/glasskube/glasskube/internal/repo/types"
 )
@@ -33,7 +34,7 @@ func (f *FakeClientset) ForRepoWithName(name string) client.RepoClient {
 }
 
 // ForPackage implements client.RepoClientset.
-func (f *FakeClientset) ForPackage(pkg v1alpha1.Package) client.RepoClient {
+func (f *FakeClientset) ForPackage(pkg ctrlpkg.Package) client.RepoClient {
 	return f.Client
 }
 
diff --git a/internal/repo/client/repoclient.go b/internal/repo/client/repoclient.go
index 168db45..cec7e64 100644
--- a/internal/repo/client/repoclient.go
+++ b/internal/repo/client/repoclient.go
@@ -2,6 +2,7 @@ package client
 
 import (
 	packagesv1alpha1 "github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/repo/types"
 )
 
@@ -25,7 +26,7 @@ type RepoMetaclient interface {
 }
 
 type RepoClientset interface {
-	ForPackage(pkg packagesv1alpha1.Package) RepoClient
+	ForPackage(pkg ctrlpkg.Package) RepoClient
 	ForRepoWithName(name string) RepoClient
 	ForRepo(repo packagesv1alpha1.PackageRepository) RepoClient
 	Default() RepoClient
diff --git a/internal/telemetry/operator.go b/internal/telemetry/operator.go
index d418e8d..e5ebd36 100644
--- a/internal/telemetry/operator.go
+++ b/internal/telemetry/operator.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/telemetry/properties"
 	"github.com/glasskube/glasskube/pkg/condition"
 	"github.com/posthog/posthog-go"
@@ -105,14 +106,14 @@ func (t *OperatorTelemetry) OnEvent(obj client.Object, status condition.Type, re
 	})
 }
 
-func (t *OperatorTelemetry) ReconcilePackage(pkg *v1alpha1.Package) {
+func (t *OperatorTelemetry) ReconcilePackage(pkg ctrlpkg.Package) {
 	if !t.Enabled() || t.posthog == nil {
 		return
 	}
 	go func() {
 		t.packageReportTimesMutex.Lock()
 		defer t.packageReportTimesMutex.Unlock()
-		if lastReported, ok := t.packageReportTimes[pkg.Name]; ok &&
+		if lastReported, ok := t.packageReportTimes[pkg.GetName()]; ok &&
 			lastReported.Add(t.packageReportMuteDuration).After(time.Now()) {
 			return
 		}
@@ -125,12 +126,12 @@ func (t *OperatorTelemetry) ReconcilePackage(pkg *v1alpha1.Package) {
 			),
 		})
 		if err == nil {
-			t.packageReportTimes[pkg.Name] = time.Now()
+			t.packageReportTimes[pkg.GetName()] = time.Now()
 		}
 	}()
 }
 
-func (t *OperatorTelemetry) ReportDelete(pkg *v1alpha1.Package) {
+func (t *OperatorTelemetry) ReportDelete(pkg ctrlpkg.Package) {
 	if !t.Enabled() || t.posthog == nil {
 		return
 	}
diff --git a/internal/telemetry/properties/builder.go b/internal/telemetry/properties/builder.go
index 3b5c12e..c95aac2 100644
--- a/internal/telemetry/properties/builder.go
+++ b/internal/telemetry/properties/builder.go
@@ -3,8 +3,8 @@ package properties
 import (
 	"runtime"
 
-	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/config"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/pkg/condition"
 	"github.com/posthog/posthog-go"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -72,16 +72,17 @@ func FromMap(data map[string]any) PropertiesBuilderFn {
 	}
 }
 
-func FromPackage(pkg *v1alpha1.Package) PropertiesBuilderFn {
+func FromPackage(pkg ctrlpkg.Package) PropertiesBuilderFn {
 	return func(p posthog.Properties) posthog.Properties {
-		p.Set("package_name", pkg.Spec.PackageInfo.Name).
-			Set("package_version_desired", pkg.Spec.PackageInfo.Version).
-			Set("package_version_actual", pkg.Status.Version).
+		p.Set("package_kind", pkg.GroupVersionKind().Kind).
+			Set("package_name", pkg.GetSpec().PackageInfo.Name).
+			Set("package_version_desired", pkg.GetSpec().PackageInfo.Version).
+			Set("package_version_actual", pkg.GetStatus().Version).
 			// TODO: set_once ?
-			Set("package_creation_timestamp", pkg.CreationTimestamp).
+			Set("package_creation_timestamp", pkg.GetCreationTimestamp()).
 			Set("package_auto_update", pkg.AutoUpdatesEnabled()).
-			Set("package_repository_name", pkg.Spec.PackageInfo.RepositoryName)
-		if c := meta.FindStatusCondition(pkg.Status.Conditions, string(condition.Ready)); c != nil {
+			Set("package_repository_name", pkg.GetSpec().PackageInfo.RepositoryName)
+		if c := meta.FindStatusCondition(pkg.GetStatus().Conditions, string(condition.Ready)); c != nil {
 			p.Set("package_ready_status", c.Status)
 			p.Set("package_ready_reason", c.Reason)
 		}
diff --git a/internal/util/slices.go b/internal/util/slices.go
index 2656e4c..251ec30 100644
--- a/internal/util/slices.go
+++ b/internal/util/slices.go
@@ -17,3 +17,15 @@ func SortBy[S ~[]E, E any, P cmp.Ordered](s S, predicate func(e E) P) {
 		}
 	})
 }
+
+func DeleteAll[S ~[]E, E comparable](s S, e E) S {
+	i := 0
+	for j := range s {
+		if s[j] != e {
+			s[i] = s[j]
+			i++
+		}
+	}
+	clear(s[i:])
+	return s[:i]
+}
diff --git a/internal/web/components/pkg_config_input/controller.go b/internal/web/components/pkg_config_input/controller.go
index 651d21d..6169284 100644
--- a/internal/web/components/pkg_config_input/controller.go
+++ b/internal/web/components/pkg_config_input/controller.go
@@ -36,7 +36,7 @@ type pkgConfigInputInput struct {
 	DatalistOptions    *PkgConfigInputDatalistOptions
 }
 
-func getStringValue(pkg *v1alpha1.Package, valueName string, valueDefinition *v1alpha1.ValueDefinition) string {
+func getStringValue(pkg *v1alpha1.ClusterPackage, valueName string, valueDefinition *v1alpha1.ValueDefinition) string {
 	if pkg != nil {
 		if valueConfiguration, ok := pkg.Spec.Values[valueName]; ok {
 			if valueConfiguration.Value != nil {
@@ -47,7 +47,7 @@ func getStringValue(pkg *v1alpha1.Package, valueName string, valueDefinition *v1
 	return valueDefinition.DefaultValue
 }
 
-func getBoolValue(pkg *v1alpha1.Package, valueName string, valueDefinition *v1alpha1.ValueDefinition) bool {
+func getBoolValue(pkg *v1alpha1.ClusterPackage, valueName string, valueDefinition *v1alpha1.ValueDefinition) bool {
 	if valueDefinition.Type == v1alpha1.ValueTypeBoolean {
 		strVal := getStringValue(pkg, valueName, valueDefinition)
 		if valBool, err := strconv.ParseBool(strVal); err == nil {
@@ -65,7 +65,7 @@ func getLabel(valueName string, valueDefinition *v1alpha1.ValueDefinition) strin
 	return inputLabel
 }
 
-func getExistingReferenceAndKind(pkg *v1alpha1.Package, valueName string) (*v1alpha1.ValueReference, string) {
+func getExistingReferenceAndKind(pkg *v1alpha1.ClusterPackage, valueName string) (*v1alpha1.ValueReference, string) {
 	if pkg != nil {
 		if val, ok := pkg.Spec.Values[valueName]; ok {
 			if val.Value == nil && val.ValueFrom != nil {
@@ -82,7 +82,8 @@ func getExistingReferenceAndKind(pkg *v1alpha1.Package, valueName string) (*v1al
 	return nil, ""
 }
 
-func getOrCreateReference(pkg *v1alpha1.Package, valueName string, desiredRefKind *string) (v1alpha1.ValueReference, string) {
+func getOrCreateReference(
+	pkg *v1alpha1.ClusterPackage, valueName string, desiredRefKind *string) (v1alpha1.ValueReference, string) {
 	existingReference, existingRefKind := getExistingReferenceAndKind(pkg, valueName)
 	if desiredRefKind != nil && *desiredRefKind != existingRefKind {
 		return v1alpha1.ValueReference{}, *desiredRefKind
@@ -94,7 +95,7 @@ func getOrCreateReference(pkg *v1alpha1.Package, valueName string, desiredRefKin
 }
 
 func ForPkgConfigInput(
-	pkg *v1alpha1.Package,
+	pkg *v1alpha1.ClusterPackage,
 	repositoryName string,
 	selectedVersion string,
 	pkgName string,
diff --git a/internal/web/components/pkg_detail_btns/controller.go b/internal/web/components/pkg_detail_btns/controller.go
index 8962dcc..65483da 100644
--- a/internal/web/components/pkg_detail_btns/controller.go
+++ b/internal/web/components/pkg_detail_btns/controller.go
@@ -15,7 +15,7 @@ type pkgDetailBtnsInput struct {
 	Status          *client.PackageStatus
 	Manifest        *v1alpha1.PackageManifest
 	UpdateAvailable bool
-	Pkg             *v1alpha1.Package
+	Pkg             *v1alpha1.ClusterPackage
 }
 
 func getId(pkgName string) string {
@@ -26,7 +26,7 @@ func ForPkgDetailBtns(
 	pkgName string,
 	status *client.PackageStatus,
 	manifest *v1alpha1.PackageManifest,
-	pkg *v1alpha1.Package,
+	pkg *v1alpha1.ClusterPackage,
 	updateAvailable bool,
 ) *pkgDetailBtnsInput {
 	id := getId(pkgName)
diff --git a/internal/web/components/pkg_overview_btn/controller.go b/internal/web/components/pkg_overview_btn/controller.go
index f13e0f3..8c76536 100644
--- a/internal/web/components/pkg_overview_btn/controller.go
+++ b/internal/web/components/pkg_overview_btn/controller.go
@@ -16,7 +16,7 @@ type pkgOverviewBtnInput struct {
 	Status          *client.PackageStatus
 	Manifest        *v1alpha1.PackageManifest
 	UpdateAvailable bool
-	Pkg             *v1alpha1.Package
+	Pkg             *v1alpha1.ClusterPackage
 }
 
 func getButtonId(pkgName string) string {
diff --git a/internal/web/configurationvalues.go b/internal/web/configurationvalues.go
index ff5b96d..ce97eab 100644
--- a/internal/web/configurationvalues.go
+++ b/internal/web/configurationvalues.go
@@ -267,8 +267,8 @@ func (s *server) getNamespaceOptions() ([]string, error) {
 }
 
 func (s *server) getPackagesOptions(ctx context.Context) ([]string, error) {
-	var packages v1alpha1.PackageList
-	if err := s.pkgClient.Packages().GetAll(ctx, &packages); err != nil {
+	var packages v1alpha1.ClusterPackageList
+	if err := s.pkgClient.ClusterPackages().GetAll(ctx, &packages); err != nil {
 		return nil, err
 	} else {
 		options := make([]string, 0)
diff --git a/internal/web/server.go b/internal/web/server.go
index e2512cd..2c21002 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -18,17 +18,6 @@ import (
 	"sync"
 	"syscall"
 
-	"k8s.io/klog/v2"
-
-	"github.com/glasskube/glasskube/internal/web/components/pkg_config_input"
-
-	"k8s.io/client-go/informers"
-	corev1 "k8s.io/client-go/listers/core/v1"
-
-	"github.com/glasskube/glasskube/internal/repo/types"
-
-	"github.com/glasskube/glasskube/internal/util"
-
 	"github.com/Masterminds/semver/v3"
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	clientadapter "github.com/glasskube/glasskube/internal/adapter/goclient"
@@ -39,7 +28,10 @@ import (
 	"github.com/glasskube/glasskube/internal/manifestvalues"
 	"github.com/glasskube/glasskube/internal/repo"
 	repoclient "github.com/glasskube/glasskube/internal/repo/client"
+	"github.com/glasskube/glasskube/internal/repo/types"
 	"github.com/glasskube/glasskube/internal/telemetry"
+	"github.com/glasskube/glasskube/internal/util"
+	"github.com/glasskube/glasskube/internal/web/components/pkg_config_input"
 	"github.com/glasskube/glasskube/internal/web/handler"
 	"github.com/glasskube/glasskube/pkg/bootstrap"
 	"github.com/glasskube/glasskube/pkg/client"
@@ -56,11 +48,14 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/informers"
 	"k8s.io/client-go/kubernetes"
+	corev1 "k8s.io/client-go/listers/core/v1"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/clientcmd/api"
+	"k8s.io/klog/v2"
 )
 
 //go:embed root
@@ -194,7 +189,9 @@ func (s *server) Start(ctx context.Context) error {
 	router.Handle("/packages/{pkgName}/configuration/{valueName}/datalists/names", s.requireReady(s.namesDatalist))
 	router.Handle("/packages/{pkgName}/configuration/{valueName}/datalists/keys", s.requireReady(s.keysDatalist))
 	router.Handle("/settings", s.requireReady(s.settingsPage))
-	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/packages", http.StatusFound) })
+	router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		http.Redirect(w, r, "/packages", http.StatusFound)
+	})
 	http.Handle("/", s.enrichContext(router))
 
 	bindAddr := fmt.Sprintf("%v:%d", s.Host, s.Port)
@@ -325,8 +322,8 @@ func (s *server) uninstallModal(w http.ResponseWriter, r *http.Request) {
 func (s *server) uninstall(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 	pkgName := r.FormValue("packageName")
-	var pkg v1alpha1.Package
-	if err := s.pkgClient.Packages().Get(ctx, pkgName, &pkg); err != nil {
+	var pkg v1alpha1.ClusterPackage
+	if err := s.pkgClient.ClusterPackages().Get(ctx, pkgName, &pkg); err != nil {
 		s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching %v during uninstall", pkgName), "danger")
 		return
 	}
@@ -346,7 +343,13 @@ func (s *server) open(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	result, err := open.NewOpener().Open(r.Context(), pkgName, "", 0)
+	var pkg v1alpha1.ClusterPackage
+	if err := s.pkgClient.ClusterPackages().Get(r.Context(), pkgName, &pkg); err != nil {
+		s.respondAlertAndLog(w, err, "Could not get ClusterPackage", "danger")
+		return
+	}
+
+	result, err := open.NewOpener().Open(r.Context(), &pkg, "", 0)
 	if err != nil {
 		s.respondAlertAndLog(w, err, "Could not open "+pkgName, "danger")
 	} else {
@@ -511,9 +514,9 @@ func (s *server) installOrConfigurePackage(w http.ResponseWriter, r *http.Reques
 	repositoryName := r.FormValue("repositoryName")
 	selectedVersion := r.FormValue("selectedVersion")
 	enableAutoUpdate := r.FormValue("enableAutoUpdate")
-	pkg := &v1alpha1.Package{}
+	pkg := &v1alpha1.ClusterPackage{}
 	var mf v1alpha1.PackageManifest
-	if err := s.pkgClient.Packages().Get(ctx, pkgName, pkg); err != nil && !apierrors.IsNotFound(err) {
+	if err := s.pkgClient.ClusterPackages().Get(ctx, pkgName, pkg); err != nil && !apierrors.IsNotFound(err) {
 		s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching package details of %v", pkgName), "danger")
 		return
 	} else if err != nil {
@@ -549,7 +552,7 @@ func (s *server) installOrConfigurePackage(w http.ResponseWriter, r *http.Reques
 			return
 		}
 	} else {
-		if mf1, err := manifest.GetInstalledManifestForPackage(ctx, *pkg); err != nil {
+		if mf1, err := manifest.GetInstalledManifestForPackage(ctx, pkg); err != nil {
 			s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred fetching package details of %v", pkgName), "danger")
 			return
 		} else {
@@ -561,7 +564,7 @@ func (s *server) installOrConfigurePackage(w http.ResponseWriter, r *http.Reques
 		s.respondAlertAndLog(w, err, "An error occurred parsing the form", "danger")
 		return
 	} else if pkg == nil {
-		pkg = client.PackageBuilder(pkgName).
+		pkg = client.ClusterPackageBuilder(pkgName).
 			WithVersion(selectedVersion).
 			WithRepositoryName(repositoryName).
 			WithAutoUpdates(strings.ToLower(enableAutoUpdate) == "on").
@@ -577,7 +580,7 @@ func (s *server) installOrConfigurePackage(w http.ResponseWriter, r *http.Reques
 		}
 	} else {
 		pkg.Spec.Values = values
-		if err := s.pkgClient.Packages().Update(ctx, pkg); err != nil {
+		if err := s.pkgClient.ClusterPackages().Update(ctx, pkg); err != nil {
 			s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred updating package %v", pkgName), "danger")
 			return
 		}
@@ -676,7 +679,7 @@ func (s *server) advancedConfiguration(w http.ResponseWriter, r *http.Request) {
 		if repositoryName != "" {
 			pkg.Spec.PackageInfo.RepositoryName = repositoryName
 		}
-		if err := s.pkgClient.Packages().Update(ctx, pkg); err != nil {
+		if err := s.pkgClient.ClusterPackages().Update(ctx, pkg); err != nil {
 			s.respondAlertAndLog(w, err,
 				fmt.Sprintf("An error occurred updating package %v to version %v in repo %v", pkgName, selectedVersion, repositoryName),
 				"danger")
@@ -996,29 +999,29 @@ func (s *server) initPackageStoreAndController(ctx context.Context) (cache.Store
 	return cache.NewInformer(
 		&cache.ListWatch{
 			ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
-				var pkgList v1alpha1.PackageList
-				err := pkgClient.Packages().GetAll(ctx, &pkgList)
+				var pkgList v1alpha1.ClusterPackageList
+				err := pkgClient.ClusterPackages().GetAll(ctx, &pkgList)
 				return &pkgList, err
 			},
 			WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
-				return pkgClient.Packages().Watch(ctx)
+				return pkgClient.ClusterPackages().Watch(ctx)
 			},
 		},
-		&v1alpha1.Package{},
+		&v1alpha1.ClusterPackage{},
 		0,
 		cache.ResourceEventHandlerFuncs{
 			AddFunc: func(obj any) {
-				if pkg, ok := obj.(*v1alpha1.Package); ok {
+				if pkg, ok := obj.(*v1alpha1.ClusterPackage); ok {
 					s.broadcastRefreshTriggers(pkg)
 				}
 			},
 			UpdateFunc: func(oldObj, newObj any) {
-				if pkg, ok := newObj.(*v1alpha1.Package); ok {
+				if pkg, ok := newObj.(*v1alpha1.ClusterPackage); ok {
 					s.broadcastRefreshTriggers(pkg)
 				}
 			},
 			DeleteFunc: func(obj any) {
-				if pkg, ok := obj.(*v1alpha1.Package); ok {
+				if pkg, ok := obj.(*v1alpha1.ClusterPackage); ok {
 					s.broadcastRefreshTriggers(pkg)
 				}
 			},
@@ -1026,7 +1029,7 @@ func (s *server) initPackageStoreAndController(ctx context.Context) (cache.Store
 	)
 }
 
-func (s *server) broadcastRefreshTriggers(pkg *v1alpha1.Package) {
+func (s *server) broadcastRefreshTriggers(pkg *v1alpha1.ClusterPackage) {
 	s.sseHub.Broadcast <- &sse{
 		event: "refresh-pkg-overview",
 	}
diff --git a/internal/web/templates.go b/internal/web/templates.go
index 88f5975..9431e65 100644
--- a/internal/web/templates.go
+++ b/internal/web/templates.go
@@ -8,28 +8,26 @@ import (
 	"path"
 	"reflect"
 
-	"github.com/glasskube/glasskube/internal/web/components/datalist"
-
-	"github.com/glasskube/glasskube/pkg/condition"
-	"k8s.io/apimachinery/pkg/api/meta"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	"github.com/yuin/goldmark"
-	"github.com/yuin/goldmark/ast"
-	"github.com/yuin/goldmark/parser"
-	"github.com/yuin/goldmark/text"
-	"github.com/yuin/goldmark/util"
-
 	"github.com/fsnotify/fsnotify"
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	repoclient "github.com/glasskube/glasskube/internal/repo/client"
 	"github.com/glasskube/glasskube/internal/semver"
 	"github.com/glasskube/glasskube/internal/web/components/alert"
+	"github.com/glasskube/glasskube/internal/web/components/datalist"
 	"github.com/glasskube/glasskube/internal/web/components/pkg_config_input"
 	"github.com/glasskube/glasskube/internal/web/components/pkg_detail_btns"
 	"github.com/glasskube/glasskube/internal/web/components/pkg_overview_btn"
 	"github.com/glasskube/glasskube/internal/web/components/pkg_update_alert"
+	"github.com/glasskube/glasskube/pkg/condition"
+	"github.com/yuin/goldmark"
+	"github.com/yuin/goldmark/ast"
+	"github.com/yuin/goldmark/parser"
+	"github.com/yuin/goldmark/text"
+	"github.com/yuin/goldmark/util"
 	"go.uber.org/multierr"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 type templates struct {
@@ -82,10 +80,10 @@ func (t *templates) parseTemplates() {
 		"ForPkgOverviewBtn": pkg_overview_btn.ForPkgOverviewBtn,
 		"ForPkgDetailBtns":  pkg_detail_btns.ForPkgDetailBtns,
 		"ForPkgUpdateAlert": pkg_update_alert.ForPkgUpdateAlert,
-		"PackageManifestUrl": func(pkg *v1alpha1.Package) string {
-			if pkg != nil {
-				url, err := t.repoClientset.ForPackage(*pkg).
-					GetPackageManifestURL(pkg.Name, pkg.Spec.PackageInfo.Version)
+		"PackageManifestUrl": func(pkg ctrlpkg.Package) string {
+			if !pkg.IsNil() {
+				url, err := t.repoClientset.ForPackage(pkg).
+					GetPackageManifestURL(pkg.GetName(), pkg.GetSpec().PackageInfo.Version)
 				if err == nil {
 					return url
 				}
@@ -135,7 +133,7 @@ func (t *templates) parseTemplates() {
 		},
 		"IsRepoStatusReady": func(repo v1alpha1.PackageRepository) bool {
 			cond := meta.FindStatusCondition(repo.Status.Conditions, string(condition.Ready))
-			return cond != nil && cond.Status == v1.ConditionTrue
+			return cond != nil && cond.Status == metav1.ConditionTrue
 		},
 	}
 
diff --git a/internal/webhook/package_validator.go b/internal/webhook/package_validator.go
index 262305e..a93db1a 100644
--- a/internal/webhook/package_validator.go
+++ b/internal/webhook/package_validator.go
@@ -6,6 +6,7 @@ import (
 	"reflect"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/controller/owners"
 	"github.com/glasskube/glasskube/internal/dependency"
 	"github.com/glasskube/glasskube/internal/dependency/graph"
@@ -27,22 +28,33 @@ type PackageValidatingWebhook struct {
 }
 
 //+kubebuilder:webhook:path=/validate-packages-glasskube-dev-v1alpha1-package,mutating=false,failurePolicy=fail,sideEffects=None,groups=packages.glasskube.dev,resources=packages,verbs=create;update;delete,versions=v1alpha1,name=vpackage.kb.io,admissionReviewVersions=v1
+//+kubebuilder:webhook:path=/validate-packages-glasskube-dev-v1alpha1-clusterpackage,mutating=false,failurePolicy=fail,sideEffects=None,groups=packages.glasskube.dev,resources=clusterpackages,verbs=create;update;delete,versions=v1alpha1,name=vclusterpackage.kb.io,admissionReviewVersions=v1
 
 var _ webhook.CustomValidator = &PackageValidatingWebhook{}
 
 func (p *PackageValidatingWebhook) SetupWithManager(mgr ctrl.Manager) error {
 	p.OwnerManager = owners.NewOwnerManager(p.Scheme())
-	return ctrl.NewWebhookManagedBy(mgr).
-		For(&v1alpha1.Package{}).
-		WithValidator(p).
-		Complete()
+	return multierr.Combine(
+		ctrl.NewWebhookManagedBy(mgr).
+			For(&v1alpha1.Package{}).
+			WithValidator(p).
+			Complete(),
+		ctrl.NewWebhookManagedBy(mgr).
+			For(&v1alpha1.ClusterPackage{}).
+			WithValidator(p).
+			Complete(),
+	)
+
 }
 
 // ValidateCreate implements admission.CustomValidator.
-func (p *PackageValidatingWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
+func (p *PackageValidatingWebhook) ValidateCreate(
+	ctx context.Context,
+	obj runtime.Object,
+) (warnings admission.Warnings, err error) {
 	log := ctrl.LoggerFrom(ctx)
-	if pkg, ok := obj.(*v1alpha1.Package); ok {
-		log.Info("validate create", "name", pkg.Name)
+	if pkg, ok := obj.(ctrlpkg.Package); ok {
+		log.Info("validate create", "name", pkg.GetName())
 		return nil, p.validateCreateOrUpdate(ctx, pkg)
 	}
 	return nil, ErrInvalidObject
@@ -51,10 +63,10 @@ func (p *PackageValidatingWebhook) ValidateCreate(ctx context.Context, obj runti
 // ValidateUpdate implements admission.CustomValidator.
 func (p *PackageValidatingWebhook) ValidateUpdate(ctx context.Context, oldObj runtime.Object, newObj runtime.Object) (warnings admission.Warnings, err error) {
 	log := ctrl.LoggerFrom(ctx)
-	if oldPkg, ok := oldObj.(*v1alpha1.Package); ok {
-		if newPkg, ok := newObj.(*v1alpha1.Package); ok {
-			log.Info("validate update", "name", newPkg.Name)
-			if reflect.DeepEqual(oldPkg.Spec, newPkg.Spec) {
+	if oldPkg, ok := oldObj.(ctrlpkg.Package); ok {
+		if newPkg, ok := newObj.(ctrlpkg.Package); ok {
+			log.Info("validate update", "name", newPkg.GetName())
+			if reflect.DeepEqual(oldPkg.GetSpec(), newPkg.GetSpec()) {
 				// If the package info did not change, we are already done
 				return nil, nil
 			} else {
@@ -75,12 +87,13 @@ func (p *PackageValidatingWebhook) ValidateUpdate(ctx context.Context, oldObj ru
 // deleted because the dependant is not deleted.
 func (p *PackageValidatingWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (warnings admission.Warnings, err error) {
 	log := ctrl.LoggerFrom(ctx)
-	if pkg, ok := obj.(*v1alpha1.Package); ok {
-		log.Info("validate delete", "name", pkg.Name)
+	if pkg, ok := obj.(ctrlpkg.Package); ok {
+		log.Info("validate delete", "name", pkg.GetName())
 		if g, err := p.NewGraph(ctx); err != nil {
 			return nil, err
-		} else {
-			if _, err := g.ValidateDelete(pkg.Name); err != nil {
+		} else if !pkg.IsNamespaceScoped() {
+			// deletion is only validated for cluster-scoped packages
+			if _, err := g.ValidateDelete(pkg.GetName()); err != nil {
 				for _, err1 := range multierr.Errors(err) {
 					if !errors.Is(err1, &graph.DependencyError{}) {
 						return nil, err1
@@ -94,10 +107,14 @@ func (p *PackageValidatingWebhook) ValidateDelete(ctx context.Context, obj runti
 	return nil, ErrInvalidObject
 }
 
-func (p *PackageValidatingWebhook) validateCreateOrUpdate(ctx context.Context, pkg *v1alpha1.Package) error {
+func (p *PackageValidatingWebhook) validateCreateOrUpdate(ctx context.Context, pkg ctrlpkg.Package) error {
 	// We must expect that this package is not installed in this version, so the PackageInfo does not exist.
 	var manifest v1alpha1.PackageManifest
-	err := p.RepoClient.ForPackage(*pkg).FetchPackageManifest(pkg.Spec.PackageInfo.Name, pkg.Spec.PackageInfo.Version, &manifest)
+	err := p.RepoClient.ForPackage(pkg).FetchPackageManifest(
+		pkg.GetSpec().PackageInfo.Name,
+		pkg.GetSpec().PackageInfo.Version,
+		&manifest,
+	)
 	if err != nil {
 		return err
 	}
@@ -106,7 +123,7 @@ func (p *PackageValidatingWebhook) validateCreateOrUpdate(ctx context.Context, p
 		return err
 	}
 
-	if result, err := p.Validate(ctx, &manifest, pkg.Spec.PackageInfo.Version); err != nil {
+	if result, err := p.Validate(ctx, &manifest, pkg.GetSpec().PackageInfo.Version); err != nil {
 		return err
 	} else if len(result.Conflicts) > 0 {
 		// Conflicts are not allowed.
diff --git a/pkg/client/builder.go b/pkg/client/builder.go
index ab53948..df7daec 100644
--- a/pkg/client/builder.go
+++ b/pkg/client/builder.go
@@ -11,7 +11,7 @@ type packageBuilder struct {
 	values                        map[string]v1alpha1.ValueConfiguration
 }
 
-func PackageBuilder(name string) *packageBuilder {
+func ClusterPackageBuilder(name string) *packageBuilder {
 	return &packageBuilder{
 		name:   name,
 		values: make(map[string]v1alpha1.ValueConfiguration),
@@ -40,8 +40,8 @@ func (b *packageBuilder) WithValues(values map[string]v1alpha1.ValueConfiguratio
 	return b
 }
 
-func (b *packageBuilder) Build() *v1alpha1.Package {
-	pkg := v1alpha1.Package{
+func (b *packageBuilder) Build() *v1alpha1.ClusterPackage {
+	pkg := v1alpha1.ClusterPackage{
 		ObjectMeta: metav1.ObjectMeta{
 			Name: b.name,
 		},
diff --git a/pkg/client/client_base.go b/pkg/client/client_base.go
index f7cdbd0..b16e1c1 100644
--- a/pkg/client/client_base.go
+++ b/pkg/client/client_base.go
@@ -7,32 +7,40 @@ import (
 	"k8s.io/client-go/tools/cache"
 )
 
-type baseClient struct {
+type baseClientset struct {
 	restClient rest.Interface
 }
 
-func (c *baseClient) Packages() PackageInterface {
+// Packages implements PackageV1Alpha1Client.
+func (c *baseClientset) Packages(namespace string) PackageInterface {
 	return &packageClient{
 		restClient: c.restClient,
+		ns:         namespace,
 	}
 }
 
-func (c *baseClient) PackageInfos() PackageInfoInterface {
+func (c *baseClientset) ClusterPackages() ClusterPackageInterface {
+	return &clusterPackageClient{
+		restClient: c.restClient,
+	}
+}
+
+func (c *baseClientset) PackageInfos() PackageInfoInterface {
 	return &packageInfoClient{restClient: c.restClient}
 }
 
-func (c *baseClient) PackageRepositories() PackageRepositoryInterface {
+func (c *baseClientset) PackageRepositories() PackageRepositoryInterface {
 	return &packageRepositoryClient{restClient: c.restClient}
 }
 
-func (c *baseClient) WithStores(
-	packageStore cache.Store,
+func (c *baseClientset) WithStores(
+	clusterPackageStore cache.Store,
 	packageInfoStore cache.Store,
 	packageRepositoryStore cache.Store,
 ) PackageV1Alpha1Client {
 	return &cacheClientset{
 		PackageV1Alpha1Client:  c,
-		packageStore:           packageStore,
+		clusterPackageStore:    clusterPackageStore,
 		packageInfoStore:       packageInfoStore,
 		packageRepositoryStore: packageRepositoryStore,
 	}
@@ -47,7 +55,7 @@ func New(cfg *rest.Config) (PackageV1Alpha1Client, error) {
 	if err != nil {
 		return nil, err
 	}
-	return &baseClient{restClient: restClient}, err
+	return &baseClientset{restClient: restClient}, err
 }
 
 func NewOrDie(cfg *rest.Config) PackageV1Alpha1Client {
diff --git a/pkg/client/client_cached.go b/pkg/client/client_cached.go
index 472040f..1da565d 100644
--- a/pkg/client/client_cached.go
+++ b/pkg/client/client_cached.go
@@ -13,23 +13,23 @@ import (
 
 type cacheClientset struct {
 	PackageV1Alpha1Client
-	packageStore           cache.Store
+	clusterPackageStore    cache.Store
 	packageInfoStore       cache.Store
 	packageRepositoryStore cache.Store
 }
 
-func (c *cacheClientset) Packages() PackageInterface {
-	p := c.PackageV1Alpha1Client.Packages()
-	if c.packageStore == nil {
+func (c *cacheClientset) ClusterPackages() ClusterPackageInterface {
+	p := c.PackageV1Alpha1Client.ClusterPackages()
+	if c.clusterPackageStore == nil {
 		return p
 	}
-	return &readWriteCacheClient[v1alpha1.Package, v1alpha1.PackageList]{
+	return &readWriteCacheClient[v1alpha1.ClusterPackage, v1alpha1.ClusterPackageList]{
 		p,
-		readOnlyCacheClient[v1alpha1.Package, v1alpha1.PackageList]{
+		readOnlyCacheClient[v1alpha1.ClusterPackage, v1alpha1.ClusterPackageList]{
 			p,
-			c.packageStore,
-			func(items []v1alpha1.Package) v1alpha1.PackageList {
-				return v1alpha1.PackageList{Items: items}
+			c.clusterPackageStore,
+			func(items []v1alpha1.ClusterPackage) v1alpha1.ClusterPackageList {
+				return v1alpha1.ClusterPackageList{Items: items}
 			},
 		},
 	}
diff --git a/pkg/client/cluster_package.go b/pkg/client/cluster_package.go
new file mode 100644
index 0000000..8be15c0
--- /dev/null
+++ b/pkg/client/cluster_package.go
@@ -0,0 +1,66 @@
+//nolint:dupl
+package client
+
+import (
+	"context"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+)
+
+var clusterPackageGVR = v1alpha1.GroupVersion.WithResource("clusterpackages")
+
+type clusterPackageClient struct {
+	restClient rest.Interface
+}
+
+func (c *clusterPackageClient) Create(
+	ctx context.Context, pkg *v1alpha1.ClusterPackage, opts metav1.CreateOptions) error {
+	return c.restClient.Post().
+		Resource(clusterPackageGVR.Resource).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(pkg).Do(ctx).Into(pkg)
+}
+
+// Update implements PackageInterface.
+func (c *clusterPackageClient) Update(ctx context.Context, p *v1alpha1.ClusterPackage) error {
+	return c.restClient.Put().
+		Resource(clusterPackageGVR.Resource).
+		Name(p.GetName()).
+		Body(p).
+		Do(ctx).
+		Into(p)
+}
+
+func (c *clusterPackageClient) Watch(ctx context.Context) (watch.Interface, error) {
+	opts := metav1.ListOptions{Watch: true}
+	return c.restClient.Get().
+		Resource(clusterPackageGVR.Resource).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Watch(ctx)
+}
+
+func (c *clusterPackageClient) Get(ctx context.Context, pkgName string, result *v1alpha1.ClusterPackage) error {
+	return c.restClient.Get().
+		Resource(clusterPackageGVR.Resource).
+		Name(pkgName).
+		Do(ctx).Into(result)
+}
+
+func (c *clusterPackageClient) GetAll(ctx context.Context, result *v1alpha1.ClusterPackageList) error {
+	return c.restClient.Get().
+		Resource(clusterPackageGVR.Resource).
+		Do(ctx).Into(result)
+}
+
+func (c *clusterPackageClient) Delete(
+	ctx context.Context, pkg *v1alpha1.ClusterPackage, options metav1.DeleteOptions) error {
+	return c.restClient.Delete().
+		Resource(clusterPackageGVR.Resource).
+		Name(pkg.Name).
+		Body(&options).
+		Do(ctx).Into(nil)
+}
diff --git a/pkg/client/interface.go b/pkg/client/interface.go
index 83d3021..89febe8 100644
--- a/pkg/client/interface.go
+++ b/pkg/client/interface.go
@@ -10,16 +10,21 @@ import (
 )
 
 type PackageV1Alpha1Client interface {
-	Packages() PackageInterface
+	ClusterPackages() ClusterPackageInterface
+	Packages(namespace string) PackageInterface
 	PackageInfos() PackageInfoInterface
 	PackageRepositories() PackageRepositoryInterface
 	WithStores(
-		packageStore cache.Store,
+		clusterPackageStore cache.Store,
 		packageInfoStore cache.Store,
 		packageRepositoryStore cache.Store,
 	) PackageV1Alpha1Client
 }
 
+type ClusterPackageInterface interface {
+	readWriteClientInterface[v1alpha1.ClusterPackage, v1alpha1.ClusterPackageList]
+}
+
 type PackageInterface interface {
 	readWriteClientInterface[v1alpha1.Package, v1alpha1.PackageList]
 }
diff --git a/pkg/client/package.go b/pkg/client/package.go
index a65bc2d..efd13bc 100644
--- a/pkg/client/package.go
+++ b/pkg/client/package.go
@@ -5,74 +5,72 @@ import (
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/watch"
 	"k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/rest"
 )
 
-var packageGVR = v1alpha1.GroupVersion.WithResource("packages")
+const packagesResource = "packages"
 
 type packageClient struct {
 	restClient rest.Interface
+	ns         string
 }
 
-func (c *packageClient) Create(ctx context.Context, pkg *v1alpha1.Package, opts metav1.CreateOptions) error {
-	return c.restClient.Post().
-		Resource(packageGVR.Resource).
+// Create implements PackageInterface.
+func (p *packageClient) Create(ctx context.Context, target *v1alpha1.Package, opts v1.CreateOptions) error {
+	return p.restClient.Post().
+		Namespace(p.ns).
+		Resource(packagesResource).
 		VersionedParams(&opts, scheme.ParameterCodec).
-		Body(pkg).Do(ctx).Into(pkg)
+		Body(target).Do(ctx).Into(target)
 }
 
-// Update implements PackageInterface.
-func (c *packageClient) Update(ctx context.Context, p *v1alpha1.Package) error {
-	return c.restClient.Put().
-		Resource(packageGVR.Resource).
-		Name(p.GetName()).
-		Body(p).
-		Do(ctx).
-		Into(p)
-}
-
-func (c *packageClient) Watch(ctx context.Context) (watch.Interface, error) {
-	opts := metav1.ListOptions{Watch: true}
-	return c.restClient.Get().
-		Resource(packageGVR.Resource).
-		VersionedParams(&opts, scheme.ParameterCodec).
-		Watch(ctx)
+// Delete implements PackageInterface.
+func (p *packageClient) Delete(ctx context.Context, target *v1alpha1.Package, options v1.DeleteOptions) error {
+	return p.restClient.Delete().
+		Namespace(p.ns).
+		Resource(packagesResource).
+		Name(target.Name).
+		Body(&options).
+		Do(ctx).Into(nil)
 }
 
-func (c *packageClient) Get(ctx context.Context, pkgName string, result *v1alpha1.Package) error {
-	return c.restClient.Get().
-		Resource(packageGVR.Resource).
-		Name(pkgName).
-		Do(ctx).Into(result)
+// Get implements PackageInterface.
+func (p *packageClient) Get(ctx context.Context, name string, target *v1alpha1.Package) error {
+	return p.restClient.Get().
+		Namespace(p.ns).
+		Resource(packagesResource).
+		Name(name).
+		Do(ctx).Into(target)
 }
 
-func (c *packageClient) GetAll(ctx context.Context, result *v1alpha1.PackageList) error {
-	return c.restClient.Get().
-		Resource(packageGVR.Resource).
-		Do(ctx).Into(result)
+// GetAll implements PackageInterface.
+func (p *packageClient) GetAll(ctx context.Context, target *v1alpha1.PackageList) error {
+	return p.restClient.Get().
+		Namespace(p.ns).
+		Resource(packagesResource).
+		Do(ctx).Into(target)
 }
 
-func (c *packageClient) Delete(ctx context.Context, pkg *v1alpha1.Package, options metav1.DeleteOptions) error {
-	return c.restClient.Delete().
-		Resource(packageGVR.Resource).
-		Name(pkg.Name).
-		Body(&options).
-		Do(ctx).Into(nil)
+// Update implements PackageInterface.
+func (p *packageClient) Update(ctx context.Context, target *v1alpha1.Package) error {
+	return p.restClient.Put().
+		Namespace(p.ns).
+		Resource(packagesResource).
+		Name(target.Name).
+		Body(target).
+		Do(ctx).
+		Into(target)
 }
 
-// NewPackage instantiates a new v1alpha1.Package struct with the given package name
-func NewPackage(packageName, version string) *v1alpha1.Package {
-	return &v1alpha1.Package{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: packageName,
-		},
-		Spec: v1alpha1.PackageSpec{
-			PackageInfo: v1alpha1.PackageInfoTemplate{
-				Name:    packageName,
-				Version: version,
-			},
-		},
-	}
+// Watch implements PackageInterface.
+func (p *packageClient) Watch(ctx context.Context) (watch.Interface, error) {
+	opts := metav1.ListOptions{Watch: true}
+	return p.restClient.Get().
+		Namespace(p.ns).
+		Resource(packagesResource).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Watch(ctx)
 }
diff --git a/pkg/client/package_status.go b/pkg/client/package_status.go
index ed82f78..57be84b 100644
--- a/pkg/client/package_status.go
+++ b/pkg/client/package_status.go
@@ -2,6 +2,7 @@ package client
 
 import (
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/pkg/condition"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -25,12 +26,12 @@ func GetStatus(status *v1alpha1.PackageStatus) *PackageStatus {
 	return nil
 }
 
-func GetStatusOrPending(pkg *v1alpha1.Package) *PackageStatus {
-	if pkg != nil {
-		if !pkg.DeletionTimestamp.IsZero() {
+func GetStatusOrPending(pkg ctrlpkg.Package) *PackageStatus {
+	if !pkg.IsNil() {
+		if !pkg.GetDeletionTimestamp().IsZero() {
 			return NewUninstallingStatus()
 		}
-		if status := GetStatus(&pkg.Status); status != nil {
+		if status := GetStatus(pkg.GetStatus()); status != nil {
 			return status
 		} else {
 			return NewPendingStatus()
diff --git a/pkg/client/packagerepository.go b/pkg/client/packagerepository.go
index 8bf0264..2cc4f2c 100644
--- a/pkg/client/packagerepository.go
+++ b/pkg/client/packagerepository.go
@@ -1,3 +1,4 @@
+//nolint:dupl // It might be possible to refactor this using generics but for now we accept the dupliate code.
 package client
 
 import (
diff --git a/pkg/describe/describe.go b/pkg/describe/describe.go
index d5cf5ae..a44bc4d 100644
--- a/pkg/describe/describe.go
+++ b/pkg/describe/describe.go
@@ -11,17 +11,17 @@ import (
 )
 
 func DescribeInstalledPackage(ctx context.Context, pkgName string) (
-	*v1alpha1.Package, *v1alpha1.PackageManifest, error) {
+	*v1alpha1.ClusterPackage, *v1alpha1.PackageManifest, error) {
 
 	pkgClient := cliutils.PackageClient(ctx)
 	repoClient := cliutils.RepositoryClientset(ctx)
-	var pkg v1alpha1.Package
-	err := pkgClient.Packages().Get(ctx, pkgName, &pkg)
+	var pkg v1alpha1.ClusterPackage
+	err := pkgClient.ClusterPackages().Get(ctx, pkgName, &pkg)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	if installedManifest, err := manifest.GetInstalledManifestForPackage(ctx, pkg); err == nil {
+	if installedManifest, err := manifest.GetInstalledManifestForPackage(ctx, &pkg); err == nil {
 		return &pkg, installedManifest, nil
 	} else if !errors.Is(err, manifest.ErrPackageNoManifest) {
 		return nil, nil, err
@@ -29,7 +29,7 @@ func DescribeInstalledPackage(ctx context.Context, pkgName string) (
 
 	// pkg is installed, but has either no manifest or owned package info (yet): use manifest in this version from repo
 	var packageManifest v1alpha1.PackageManifest
-	err = repoClient.ForPackage(pkg).FetchPackageManifest(pkgName, pkg.Spec.PackageInfo.Version, &packageManifest)
+	err = repoClient.ForPackage(&pkg).FetchPackageManifest(pkgName, pkg.Spec.PackageInfo.Version, &packageManifest)
 	if err != nil {
 		return nil, nil, err
 	} else {
diff --git a/pkg/install/install.go b/pkg/install/install.go
index 894a6ff..bed4f09 100644
--- a/pkg/install/install.go
+++ b/pkg/install/install.go
@@ -30,7 +30,7 @@ func (obj *installer) WithStatusWriter(sw statuswriter.StatusWriter) *installer
 
 // InstallBlocking creates a new v1alpha1.Package custom resource in the cluster and waits until
 // the package has either status Ready or Failed.
-func (obj *installer) InstallBlocking(ctx context.Context, pkg *v1alpha1.Package,
+func (obj *installer) InstallBlocking(ctx context.Context, pkg *v1alpha1.ClusterPackage,
 	opts metav1.CreateOptions) (*client.PackageStatus, error) {
 	obj.status.Start()
 	defer obj.status.Stop()
@@ -53,7 +53,7 @@ func (obj *installer) InstallBlocking(ctx context.Context, pkg *v1alpha1.Package
 }
 
 // Install creates a new v1alpha1.Package custom resource in the cluster.
-func (obj *installer) Install(ctx context.Context, pkg *v1alpha1.Package,
+func (obj *installer) Install(ctx context.Context, pkg *v1alpha1.ClusterPackage,
 	opts metav1.CreateOptions) error {
 	obj.status.Start()
 	defer obj.status.Stop()
@@ -63,11 +63,11 @@ func (obj *installer) Install(ctx context.Context, pkg *v1alpha1.Package,
 
 func (obj *installer) install(
 	ctx context.Context,
-	pkg *v1alpha1.Package,
+	pkg *v1alpha1.ClusterPackage,
 	opts metav1.CreateOptions,
-) (*v1alpha1.Package, error) {
+) (*v1alpha1.ClusterPackage, error) {
 	obj.status.SetStatus(fmt.Sprintf("Installing %v...", pkg.Name))
-	err := obj.client.Packages().Create(ctx, pkg, opts)
+	err := obj.client.ClusterPackages().Create(ctx, pkg, opts)
 	if err != nil {
 		return nil, err
 	}
@@ -75,13 +75,13 @@ func (obj *installer) install(
 }
 
 func (obj *installer) awaitInstall(ctx context.Context, pkgUID types.UID) (*client.PackageStatus, error) {
-	watcher, err := obj.client.Packages().Watch(ctx)
+	watcher, err := obj.client.ClusterPackages().Watch(ctx)
 	if err != nil {
 		return nil, err
 	}
 	defer watcher.Stop()
 	for event := range watcher.ResultChan() {
-		if obj, ok := event.Object.(*v1alpha1.Package); ok && obj.GetUID() == pkgUID {
+		if obj, ok := event.Object.(*v1alpha1.ClusterPackage); ok && obj.GetUID() == pkgUID {
 			if event.Type == watch.Added || event.Type == watch.Modified {
 				if status := client.GetStatus(&obj.Status); status != nil {
 					return status, nil
diff --git a/pkg/list/lister.go b/pkg/list/lister.go
index 8569997..bcf0953 100644
--- a/pkg/list/lister.go
+++ b/pkg/list/lister.go
@@ -17,7 +17,7 @@ import (
 type PackageWithStatus struct {
 	repotypes.MetaIndexItem
 	Status            *client.PackageStatus     `json:"status,omitempty"`
-	Package           *v1alpha1.Package         `json:"package,omitempty"`
+	Package           *v1alpha1.ClusterPackage  `json:"package,omitempty"`
 	InstalledManifest *v1alpha1.PackageManifest `json:"installedmanifest,omitempty"`
 }
 
@@ -69,7 +69,7 @@ func (l *lister) fetchRepoAndInstalled(ctx context.Context, options ListOptions)
 	error,
 ) {
 	var index repotypes.MetaIndex
-	var packages v1alpha1.PackageList
+	var packages v1alpha1.ClusterPackageList
 	var packageInfos v1alpha1.PackageInfoList
 	var repoErr, pkgErr, pkgInfoErr error
 	wg := new(sync.WaitGroup)
@@ -84,7 +84,7 @@ func (l *lister) fetchRepoAndInstalled(ctx context.Context, options ListOptions)
 
 	go func() {
 		defer wg.Done()
-		if err := l.pkgClient.Packages().GetAll(ctx, &packages); err != nil {
+		if err := l.pkgClient.ClusterPackages().GetAll(ctx, &packages); err != nil {
 			pkgErr = fmt.Errorf("could not fetch installed packages: %w", err)
 		}
 	}()
@@ -109,10 +109,10 @@ func (l *lister) fetchRepoAndInstalled(ctx context.Context, options ListOptions)
 	result := make([]result, len(index.Packages))
 	for i, indexPackage := range index.Packages {
 		result[i].IndexItem = &index.Packages[i]
-		for j, clusterPackage := range packages.Items {
-			if indexPackage.Name == clusterPackage.Name {
+		for j, pkg := range packages.Items {
+			if indexPackage.Name == pkg.Name {
 				result[i].Package = &packages.Items[j]
-				packageInfoName := names.PackageInfoName(clusterPackage)
+				packageInfoName := names.PackageInfoName(&pkg)
 				for k, packageInfo := range packageInfos.Items {
 					if packageInfo.Name == packageInfoName {
 						result[i].PackageInfo = &packageInfos.Items[k]
diff --git a/pkg/list/result.go b/pkg/list/result.go
index 0190906..28cebcb 100644
--- a/pkg/list/result.go
+++ b/pkg/list/result.go
@@ -7,7 +7,7 @@ import (
 
 type result struct {
 	IndexItem    *types.MetaIndexItem
-	Package      *v1alpha1.Package
+	Package      *v1alpha1.ClusterPackage
 	PackageInfo  *v1alpha1.PackageInfo
 	Repositories []*v1alpha1.PackageRepository
 }
diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go
index f58a4d7..3213772 100644
--- a/pkg/manifest/manifest.go
+++ b/pkg/manifest/manifest.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/cliutils"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/internal/names"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 )
@@ -15,14 +16,14 @@ var ErrPackageNoManifest = errors.New("package has no manifest")
 
 func GetInstalledManifest(ctx context.Context, pkgName string) (*v1alpha1.PackageManifest, error) {
 	pkgClient := cliutils.PackageClient(ctx)
-	var pkg v1alpha1.Package
-	if err := pkgClient.Packages().Get(ctx, pkgName, &pkg); err != nil {
+	var pkg v1alpha1.ClusterPackage
+	if err := pkgClient.ClusterPackages().Get(ctx, pkgName, &pkg); err != nil {
 		return nil, err
 	}
-	return GetInstalledManifestForPackage(ctx, pkg)
+	return GetInstalledManifestForPackage(ctx, &pkg)
 }
 
-func GetInstalledManifestForPackage(ctx context.Context, pkg v1alpha1.Package) (*v1alpha1.PackageManifest, error) {
+func GetInstalledManifestForPackage(ctx context.Context, pkg ctrlpkg.Package) (*v1alpha1.PackageManifest, error) {
 	pkgClient := cliutils.PackageClient(ctx)
 	var packageInfo v1alpha1.PackageInfo
 	if err := pkgClient.PackageInfos().Get(ctx, names.PackageInfoName(pkg), &packageInfo); err != nil {
diff --git a/pkg/open/open.go b/pkg/open/open.go
index 7d1c2f1..1503b20 100644
--- a/pkg/open/open.go
+++ b/pkg/open/open.go
@@ -11,6 +11,7 @@ import (
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/clicontext"
 	"github.com/glasskube/glasskube/internal/cliutils"
+	"github.com/glasskube/glasskube/internal/controller/ctrlpkg"
 	"github.com/glasskube/glasskube/pkg/future"
 	"github.com/glasskube/glasskube/pkg/manifest"
 	corev1 "k8s.io/api/core/v1"
@@ -36,14 +37,16 @@ func NewOpener() *opener {
 	return &opener{}
 }
 
-func (o *opener) Open(ctx context.Context, packageName string, entrypointName string, port int32) (*OpenResult, error) {
+func (o *opener) Open(
+	ctx context.Context, pkg ctrlpkg.Package, entrypointName string, port int32) (*OpenResult, error) {
+
 	if err := o.initFromContext(ctx); err != nil {
 		return nil, err
 	}
 
-	manifest, err := manifest.GetInstalledManifest(ctx, packageName)
+	manifest, err := manifest.GetInstalledManifestForPackage(ctx, pkg)
 	if err != nil {
-		return nil, fmt.Errorf("could not get PackageInfo for package %v: %w", packageName, err)
+		return nil, fmt.Errorf("could not get PackageInfo for %v %v: %w", pkg.GetSpec().PackageInfo.Name, pkg.GetName(), err)
 	}
 
 	if len(manifest.Entrypoints) < 1 {
@@ -67,6 +70,11 @@ func (o *opener) Open(ctx context.Context, packageName string, entrypointName st
 		}
 	}
 
+	namespace := pkg.GetNamespace()
+	if namespace == "" {
+		namespace = manifest.DefaultNamespace
+	}
+
 	result := OpenResult{opener: o}
 	var futures []future.Future
 	for _, entrypoint := range manifest.Entrypoints {
@@ -79,7 +87,7 @@ func (o *opener) Open(ctx context.Context, packageName string, entrypointName st
 			stopCh := make(chan struct{})
 			o.readyCh = append(o.readyCh, readyCh)
 			o.stopCh = append(o.stopCh, stopCh)
-			entrypointFuture, err := o.open(ctx, manifest, e, readyCh, stopCh)
+			entrypointFuture, err := o.open(ctx, namespace, e, readyCh, stopCh)
 			if err != nil {
 				o.stop()
 				epName := e.Name
@@ -131,7 +139,7 @@ func (o *opener) stop() {
 
 func (o *opener) open(
 	ctx context.Context,
-	manifest *v1alpha1.PackageManifest,
+	namespace string,
 	entrypoint v1alpha1.PackageEntrypoint,
 	readyChannel chan struct{},
 	stopChannel chan struct{},
@@ -140,7 +148,7 @@ func (o *opener) open(
 		return nil, err
 	}
 
-	svc, err := o.service(ctx, manifest, entrypoint)
+	svc, err := o.service(ctx, namespace, entrypoint)
 	if err != nil {
 		return nil, err
 	}
@@ -179,11 +187,11 @@ func (o *opener) open(
 
 func (o *opener) service(
 	ctx context.Context,
-	manifest *v1alpha1.PackageManifest,
+	namespace string,
 	entrypoint v1alpha1.PackageEntrypoint,
 ) (*corev1.Service, error) {
 	svc, err := o.ksClient.CoreV1().
-		Services(manifest.DefaultNamespace).
+		Services(namespace).
 		Get(ctx, entrypoint.ServiceName, metav1.GetOptions{})
 	if err != nil {
 		return nil, fmt.Errorf("could not get service %v: %w", entrypoint.ServiceName, err)
@@ -208,7 +216,7 @@ func (o *opener) pod(ctx context.Context, service *corev1.Service) (*corev1.Pod,
 			return &pod, nil
 		}
 	}
-	return nil, fmt.Errorf("no ready pod found for service %v", service.Name)
+	return nil, fmt.Errorf("no pod found for service %v has status ready", service.Name)
 }
 
 func portMapping(service *corev1.Service, pod *corev1.Pod, entrypoint v1alpha1.PackageEntrypoint) (string, error) {
diff --git a/pkg/uninstall/uninstall.go b/pkg/uninstall/uninstall.go
index ade4bb5..6ca7e00 100644
--- a/pkg/uninstall/uninstall.go
+++ b/pkg/uninstall/uninstall.go
@@ -30,7 +30,7 @@ func (obj *uninstaller) WithStatusWriter(sw statuswriter.StatusWriter) *uninstal
 
 // UninstallBlocking deletes the v1alpha1.Package custom resource from the
 // cluster and waits until the package is fully deleted.
-func (obj *uninstaller) UninstallBlocking(ctx context.Context, pkg *v1alpha1.Package) error {
+func (obj *uninstaller) UninstallBlocking(ctx context.Context, pkg *v1alpha1.ClusterPackage) error {
 	obj.status.Start()
 	defer obj.status.Stop()
 	err := obj.delete(ctx, pkg)
@@ -41,25 +41,26 @@ func (obj *uninstaller) UninstallBlocking(ctx context.Context, pkg *v1alpha1.Pac
 }
 
 // Uninstall deletes the v1alpha1.Package custom resource from the cluster.
-func (obj *uninstaller) Uninstall(ctx context.Context, pkg *v1alpha1.Package) error {
+func (obj *uninstaller) Uninstall(ctx context.Context, pkg *v1alpha1.ClusterPackage) error {
 	obj.status.Start()
 	defer obj.status.Stop()
 	return obj.delete(ctx, pkg)
 }
 
-func (obj *uninstaller) delete(ctx context.Context, pkg *v1alpha1.Package) error {
+func (obj *uninstaller) delete(ctx context.Context, pkg *v1alpha1.ClusterPackage) error {
 	obj.status.SetStatus(fmt.Sprintf("Uninstalling %v...", pkg.Name))
-	return obj.client.Packages().Delete(ctx, pkg, metav1.DeleteOptions{PropagationPolicy: &deletePropagationForeground})
+	return obj.client.ClusterPackages().
+		Delete(ctx, pkg, metav1.DeleteOptions{PropagationPolicy: &deletePropagationForeground})
 }
 
 func (obj *uninstaller) awaitDeletion(ctx context.Context, name string) error {
-	watcher, err := obj.client.Packages().Watch(ctx)
+	watcher, err := obj.client.ClusterPackages().Watch(ctx)
 	if err != nil {
 		return err
 	}
 	defer watcher.Stop()
 	for event := range watcher.ResultChan() {
-		if pkg, ok := event.Object.(*v1alpha1.Package); ok && pkg.Name == name {
+		if pkg, ok := event.Object.(*v1alpha1.ClusterPackage); ok && pkg.Name == name {
 			if event.Type == watch.Deleted {
 				return nil // Package deletion confirmed
 			}
diff --git a/pkg/update/update.go b/pkg/update/update.go
index 5e34a52..8332503 100644
--- a/pkg/update/update.go
+++ b/pkg/update/update.go
@@ -34,7 +34,7 @@ func (tx UpdateTransaction) IsEmpty() bool {
 }
 
 type updateTransactionItem struct {
-	Package v1alpha1.Package
+	Package v1alpha1.ClusterPackage
 	Version string
 }
 
@@ -76,8 +76,8 @@ func (c *updater) PrepareForVersion(
 	defer c.status.Stop()
 	c.status.SetStatus("Collecting installed package")
 
-	var pkg v1alpha1.Package
-	if err := c.client.Packages().Get(ctx, pkgName, &pkg); err != nil {
+	var pkg v1alpha1.ClusterPackage
+	if err := c.client.ClusterPackages().Get(ctx, pkgName, &pkg); err != nil {
 		return nil, fmt.Errorf("failed to get package %v: %v", pkgName, err)
 	}
 
@@ -90,7 +90,7 @@ func (c *updater) PrepareForVersion(
 	var tx UpdateTransaction
 	item := updateTransactionItem{Package: pkg, Version: pkgVersion}
 	var manifest v1alpha1.PackageManifest
-	if err := c.repoClient.ForPackage(pkg).FetchPackageManifest(pkg.Name, pkgVersion, &manifest); err != nil {
+	if err := c.repoClient.ForPackage(&pkg).FetchPackageManifest(pkg.Name, pkgVersion, &manifest); err != nil {
 		return nil, err
 	} else if result, err := c.dm.Validate(ctx, &manifest, pkgVersion); err != nil {
 		return nil, err
@@ -107,20 +107,20 @@ func (c *updater) Prepare(ctx context.Context, packageNames []string) (*UpdateTr
 	c.status.Start()
 	defer c.status.Stop()
 	c.status.SetStatus("Collecting installed packages")
-	var packagesToUpdate []v1alpha1.Package
+	var packagesToUpdate []v1alpha1.ClusterPackage
 	if len(packageNames) > 0 {
 		// Fetch all requested packages individually.
 		// This way, we can fail early if a requested package is not installed.
 		for _, name := range packageNames {
-			var pkg v1alpha1.Package
-			if err := c.client.Packages().Get(ctx, name, &pkg); err != nil {
+			var pkg v1alpha1.ClusterPackage
+			if err := c.client.ClusterPackages().Get(ctx, name, &pkg); err != nil {
 				return nil, fmt.Errorf("failed to get package %v: %v", name, err)
 			}
 			packagesToUpdate = append(packagesToUpdate, pkg)
 		}
 	} else {
-		var packageList v1alpha1.PackageList
-		if err := c.client.Packages().GetAll(ctx, &packageList); err != nil {
+		var packageList v1alpha1.ClusterPackageList
+		if err := c.client.ClusterPackages().GetAll(ctx, &packageList); err != nil {
 			return nil, fmt.Errorf("failed to get list of installed packages: %v", err)
 		}
 		packagesToUpdate = packageList.Items
@@ -132,7 +132,7 @@ func (c *updater) Prepare(ctx context.Context, packageNames []string) (*UpdateTr
 	var tx UpdateTransaction
 outer:
 	for _, pkg := range packagesToUpdate {
-		repoClient := c.repoClient.ForPackage(pkg)
+		repoClient := c.repoClient.ForPackage(&pkg)
 		var index repo.PackageRepoIndex
 		if err := repoClient.FetchPackageRepoIndex(&index); err != nil {
 			return nil, fmt.Errorf("failed to fetch index: %v", err)
@@ -176,10 +176,10 @@ outer:
 	return &tx, nil
 }
 
-func (c *updater) Apply(ctx context.Context, tx *UpdateTransaction) ([]v1alpha1.Package, error) {
+func (c *updater) Apply(ctx context.Context, tx *UpdateTransaction) ([]v1alpha1.ClusterPackage, error) {
 	c.status.Start()
 	defer c.status.Stop()
-	var updatedPackages []v1alpha1.Package
+	var updatedPackages []v1alpha1.ClusterPackage
 	for _, item := range tx.Items {
 		if item.UpdateRequired() {
 			c.status.SetStatus(fmt.Sprintf("Updating %v", item.Package.Name))
@@ -196,19 +196,19 @@ func (c *updater) Apply(ctx context.Context, tx *UpdateTransaction) ([]v1alpha1.
 	return updatedPackages, nil
 }
 
-func (c *updater) UpdatePackage(ctx context.Context, pkg *v1alpha1.Package, version string) error {
+func (c *updater) UpdatePackage(ctx context.Context, pkg *v1alpha1.ClusterPackage, version string) error {
 	pkg.Spec.PackageInfo.Version = version
-	return c.client.Packages().Update(ctx, pkg)
+	return c.client.ClusterPackages().Update(ctx, pkg)
 }
 
-func (c *updater) awaitUpdate(ctx context.Context, pkg *v1alpha1.Package) error {
-	watcher, err := c.client.Packages().Watch(ctx)
+func (c *updater) awaitUpdate(ctx context.Context, pkg *v1alpha1.ClusterPackage) error {
+	watcher, err := c.client.ClusterPackages().Watch(ctx)
 	if err != nil {
 		return err
 	}
 	defer watcher.Stop()
 	for event := range watcher.ResultChan() {
-		if eventPkg, ok := event.Object.(*v1alpha1.Package); ok && eventPkg.GetUID() == pkg.GetUID() {
+		if eventPkg, ok := event.Object.(*v1alpha1.ClusterPackage); ok && eventPkg.GetUID() == pkg.GetUID() {
 			if eventPkg.Status.Version == eventPkg.Spec.PackageInfo.Version {
 				return nil
 			}
diff --git a/website/static/schemas/v1/package-manifest.json b/website/static/schemas/v1/package-manifest.json
index 9c9567d..193dfd1 100644
--- a/website/static/schemas/v1/package-manifest.json
+++ b/website/static/schemas/v1/package-manifest.json
@@ -90,6 +90,12 @@
         "url"
       ]
     },
+    "PackageScope": {
+      "enum": [
+        "Cluster",
+        "Namespaced"
+      ]
+    },
     "PartialJsonPatch": {
       "properties": {
         "op": {
@@ -266,6 +272,9 @@
     }
   },
   "properties": {
+    "scope": {
+      "$ref": "#/$defs/PackageScope"
+    },
     "name": {
       "type": "string"
     },


Test patch:
diff --git a/internal/dependency/graph/graph_test.go b/internal/dependency/graph/graph_test.go
index 5a134cd..7c3cee9 100644
--- a/internal/dependency/graph/graph_test.go
+++ b/internal/dependency/graph/graph_test.go
@@ -26,7 +26,7 @@ var _ = Describe("DependencyGraph", func() {
 
 		When("there is a package without dependencies", func() {
 			It("should not return an error", func() {
-				Expect(graph.Add(foo, "v1.0.0", nil, true)).NotTo(HaveOccurred())
+				Expect(graph.AddCluster(foo, "v1.0.0", nil, true)).NotTo(HaveOccurred())
 				Expect(graph.Validate()).NotTo(HaveOccurred())
 			})
 		})
@@ -35,24 +35,24 @@ var _ = Describe("DependencyGraph", func() {
 			When("the dependency exists", func() {
 				When("there is a constraint", func() {
 					It("should not return an error", func() {
-						Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
-						Expect(graph.Add(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
 						Expect(graph.Validate()).NotTo(HaveOccurred())
 					})
 				})
 
 				When("there is a constraint and it is not violated", func() {
 					It("should not return an error", func() {
-						Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}}, true)).NotTo(HaveOccurred())
-						Expect(graph.Add(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}}, true)).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
 						Expect(graph.Validate()).NotTo(HaveOccurred())
 					})
 				})
 
 				When("there is a constraint and it is violated", func() {
 					It("should return an error", func() {
-						Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.1.x"}}, true)).NotTo(HaveOccurred())
-						Expect(graph.Add(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.1.x"}}, true)).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
 						Expect(graph.Validate()).To(MatchError(&DependencyError{}))
 					})
 				})
@@ -60,7 +60,42 @@ var _ = Describe("DependencyGraph", func() {
 
 			When("the dependency does not exist", func() {
 				It("should return an error", func() {
-					Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
+					Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
+					Expect(graph.Validate()).To(MatchError(&DependencyError{}))
+				})
+			})
+		})
+
+		When("there is a namespaced package with dependencies", func() {
+			When("the dependency exists", func() {
+				When("there is a constraint", func() {
+					It("should not return an error", func() {
+						Expect(graph.AddNamespaced(foo, foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}})).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+						Expect(graph.Validate()).NotTo(HaveOccurred())
+					})
+				})
+
+				When("there is a constraint and it is not violated", func() {
+					It("should not return an error", func() {
+						Expect(graph.AddNamespaced(foo, foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}})).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+						Expect(graph.Validate()).NotTo(HaveOccurred())
+					})
+				})
+
+				When("there is a constraint and it is violated", func() {
+					It("should return an error", func() {
+						Expect(graph.AddNamespaced(foo, foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.1.x"}})).NotTo(HaveOccurred())
+						Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+						Expect(graph.Validate()).To(MatchError(&DependencyError{}))
+					})
+				})
+			})
+
+			When("the dependency does not exist", func() {
+				It("should return an error", func() {
+					Expect(graph.AddNamespaced(foo, foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}})).NotTo(HaveOccurred())
 					Expect(graph.Validate()).To(MatchError(&DependencyError{}))
 				})
 			})
@@ -69,7 +104,7 @@ var _ = Describe("DependencyGraph", func() {
 
 	Describe("Delete", func() {
 		It("should remove all properties", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
 			Expect(graph.Version(foo)).NotTo(BeNil())
 			Expect(graph.Manual(foo)).To(BeTrue())
 			Expect(graph.Dependencies(foo)).NotTo(BeEmpty())
@@ -82,7 +117,7 @@ var _ = Describe("DependencyGraph", func() {
 
 	Describe("Prune", func() {
 		It("should remove orphaned vertex", func() {
-			Expect(graph.Add(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
 			Expect(graph.Version(bar)).To(Equal(semver.MustParse("v1.0.0")))
 			Expect(graph.Prune()).To(ConsistOf(bar))
 			Expect(graph.Version(bar)).To(BeNil())
@@ -91,8 +126,8 @@ var _ = Describe("DependencyGraph", func() {
 
 	Describe("DeleteAndPrune", func() {
 		It("should remove dependency", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
-			Expect(graph.Add(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
 			Expect(graph.Version(bar)).To(Equal(semver.MustParse("v1.0.0")))
 			Expect(graph.DeleteAndPrune(foo)).To(ConsistOf(foo, bar))
 			Expect(graph.Version(bar)).To(BeNil())
@@ -101,18 +136,18 @@ var _ = Describe("DependencyGraph", func() {
 
 	Describe("Max", func() {
 		It("should return error for empty slice", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.0.0, < 1.1.2"}}, true)).
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.0.0, < 1.1.2"}}, true)).
 				NotTo(HaveOccurred())
-			Expect(graph.Add(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.1.0"}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.1.0"}}, true)).NotTo(HaveOccurred())
 			v, err := graph.Max(bar, []*semver.Version{})
 			Expect(err).To(HaveOccurred())
 			Expect(v).To(BeNil())
 		})
 
 		It("should return error for no matching version", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.0.0, < 1.1.1"}}, true)).
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.0.0, < 1.1.1"}}, true)).
 				NotTo(HaveOccurred())
-			Expect(graph.Add(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.1.0"}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.1.0"}}, true)).NotTo(HaveOccurred())
 			versions := []*semver.Version{semver.MustParse("1.0.0"), semver.MustParse("1.1.1"), semver.MustParse("1.2.0"),
 				semver.MustParse("2.0.0")}
 			v, err := graph.Max(bar, versions)
@@ -121,9 +156,9 @@ var _ = Describe("DependencyGraph", func() {
 		})
 
 		It("should return correct version", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.0.0, < 1.1.1"}}, true)).
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.0.0, < 1.1.1"}}, true)).
 				NotTo(HaveOccurred())
-			Expect(graph.Add(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.1.0"}}, true)).
+			Expect(graph.AddCluster(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: ">= 1.1.0"}}, true)).
 				NotTo(HaveOccurred())
 			versions := []*semver.Version{semver.MustParse("1.0.0"), semver.MustParse("1.1.0"), semver.MustParse("1.1.1"),
 				semver.MustParse("1.2.0"), semver.MustParse("2.0.0")}
@@ -135,7 +170,7 @@ var _ = Describe("DependencyGraph", func() {
 
 	Describe("Dependencies", func() {
 		It("should return all dependencies", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}, {Name: baz}}, true)).
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}, {Name: baz}}, true)).
 				NotTo(HaveOccurred())
 			Expect(graph.Dependencies(foo)).To(ConsistOf(bar, baz))
 		})
@@ -143,31 +178,46 @@ var _ = Describe("DependencyGraph", func() {
 
 	Describe("Dependants", func() {
 		It("should return all dependants", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}}, true)).NotTo(HaveOccurred())
-			Expect(graph.Add(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
-			Expect(graph.Add(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
-			Expect(graph.Dependants(bar)).To(ConsistOf(foo, baz))
+			Expect(graph.AddNamespaced(foo, foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}})).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+			Expect(graph.Dependants(bar)).To(ContainElements(foo, baz))
+			Expect(graph.Dependants(bar)).To(HaveLen(3))
 		})
 	})
 
 	Describe("Constraints", func() {
 		It("should return constraints of dependants", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}}, true)).NotTo(HaveOccurred())
-			Expect(graph.Add(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
-			Expect(graph.Add(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
-			Expect(graph.Constraints(bar)).To(ConsistOf(constraint("1.x.x")))
+			Expect(graph.AddNamespaced(foo, foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.2.x"}})).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(baz, "v1.0.0", []v1alpha1.Dependency{{Name: bar}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+			Expect(graph.Constraints(bar)).To(ConsistOf(constraint("1.x.x"), constraint("1.2.x")))
 		})
 	})
 
 	Describe("DeepCopy", func() {
 		It("should produce equal graph", func() {
-			Expect(graph.Add(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}}, true)).NotTo(HaveOccurred())
-			Expect(graph.Add(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(foo, "v1.0.0", []v1alpha1.Dependency{{Name: bar, Version: "1.x.x"}}, true)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(bar, "v1.0.0", nil, false)).NotTo(HaveOccurred())
 			newGraph := graph.DeepCopy()
-			Expect(graph.Add(bar, "v1.1.0", nil, false)).NotTo(HaveOccurred())
+			Expect(graph.AddCluster(bar, "v1.1.0", nil, false)).NotTo(HaveOccurred())
 			Expect(newGraph).NotTo(Equal(graph))
 		})
 	})
+
+	Describe("Version", func() {
+		It("should return nil for missing package", func() {
+			Expect(graph.Version("foo")).To(BeNil())
+		})
+	})
+
+	Describe("Manual", func() {
+		It("should return false for missing package", func() {
+			Expect(graph.Manual("foo")).To(BeFalse())
+		})
+	})
 })
 
 func constraint(pattern string) *semver.Constraints {
diff --git a/internal/dependency/manager_test.go b/internal/dependency/manager_test.go
index 32bd8bd..e0ccca8 100644
--- a/internal/dependency/manager_test.go
+++ b/internal/dependency/manager_test.go
@@ -2,6 +2,7 @@ package dependency
 
 import (
 	"context"
+	"slices"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/names"
@@ -20,8 +21,9 @@ var fakeRepo = &fake.FakeClient{
 }
 
 type testClientAdapter struct {
-	packages     []v1alpha1.Package
-	packageInfos []v1alpha1.PackageInfo
+	clusterPackages []v1alpha1.ClusterPackage
+	packages        []v1alpha1.Package
+	packageInfos    []v1alpha1.PackageInfo
 }
 
 func (a *testClientAdapter) GetPackageInfo(ctx context.Context, pkgInfoName string) (*v1alpha1.PackageInfo, error) {
@@ -33,12 +35,21 @@ func (a *testClientAdapter) GetPackageInfo(ctx context.Context, pkgInfoName stri
 	return nil, errors.NewNotFound(schema.GroupResource{}, pkgInfoName)
 }
 
-func (a *testClientAdapter) ListPackages(ctx context.Context) (*v1alpha1.PackageList, error) {
-	return &v1alpha1.PackageList{Items: a.packages}, nil
+// ListPackages implements adapter.PackageClientAdapter.
+func (a *testClientAdapter) ListPackages(ctx context.Context, namespace string) (*v1alpha1.PackageList, error) {
+	return &v1alpha1.PackageList{
+		Items: slices.DeleteFunc(a.packages[:], func(pkg v1alpha1.Package) bool {
+			return namespace != "" && pkg.Namespace != namespace
+		}),
+	}, nil
 }
 
-// GetPackage implements adapter.PackageClientAdapter.
-func (a *testClientAdapter) GetPackage(ctx context.Context, name string) (*v1alpha1.Package, error) {
+func (a *testClientAdapter) ListClusterPackages(ctx context.Context) (*v1alpha1.ClusterPackageList, error) {
+	return &v1alpha1.ClusterPackageList{Items: a.clusterPackages}, nil
+}
+
+// GetClusterPackage implements adapter.PackageClientAdapter.
+func (a *testClientAdapter) GetClusterPackage(ctx context.Context, name string) (*v1alpha1.ClusterPackage, error) {
 	panic("unimplemented")
 }
 
@@ -66,41 +77,54 @@ var dm *DependendcyManager
 // Package d (name "D") is the dependency (such that P depends on D) or does not exist, and Packages x (name "X") and
 // y (name "Y") are additional optional packages having a dependency on D. For tests where P has multiple dependencies,
 // we additionally use Package e (name "E").
-var d, e, p, x, y *v1alpha1.Package
+var d, e, p, x, y *v1alpha1.ClusterPackage
+
+// Package n (name "N") is a namespace-scoped package that depends on E
+var n *v1alpha1.Package
 
-// di, ei, pi, xi, yi are the corresponding PackageInfo's to the package d, p, x, y
-var di, ei, pi, xi, yi *v1alpha1.PackageInfo
+// di, ei, pi, xi, yi, ni are the corresponding PackageInfo's to the package d, p, x, y, n
+var di, ei, pi, xi, yi, ni *v1alpha1.PackageInfo
+
+func createClusterPackageAndInfo(
+	name, version string, installed bool) (*v1alpha1.ClusterPackage, *v1alpha1.PackageInfo) {
 
-func createPackageAndInfo(name string, version string, installed bool) (*v1alpha1.Package, *v1alpha1.PackageInfo) {
 	manifest := v1alpha1.PackageManifest{
 		Name: name,
 	}
 	fakeRepo.AddPackage(name, version, &manifest)
-	pkg := v1alpha1.Package{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: name,
-		},
-		Spec: v1alpha1.PackageSpec{
-			PackageInfo: v1alpha1.PackageInfoTemplate{
-				Name:    name,
-				Version: version,
-			},
-		},
-		Status: v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: name}}},
+	pkg := v1alpha1.ClusterPackage{
+		ObjectMeta: metav1.ObjectMeta{Name: name},
+		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: name, Version: version}},
+		Status:     v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: name}}},
+	}
+	pkgi := v1alpha1.PackageInfo{
+		ObjectMeta: metav1.ObjectMeta{Name: names.PackageInfoName(&pkg)},
+		Spec:       v1alpha1.PackageInfoSpec{Name: name, Version: version},
+		Status:     v1alpha1.PackageInfoStatus{Version: version, Manifest: &manifest},
+	}
+	if installed {
+		testClient.clusterPackages = append(testClient.clusterPackages, pkg)
+		testClient.packageInfos = append(testClient.packageInfos, pkgi)
 	}
+	return &pkg, &pkgi
+}
 
+func createPackageAndInfo(
+	name, namespace, version string, installed bool) (*v1alpha1.Package, *v1alpha1.PackageInfo) {
+
+	manifest := v1alpha1.PackageManifest{
+		Name: name,
+	}
+	fakeRepo.AddPackage(name, version, &manifest)
+	pkg := v1alpha1.Package{
+		ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
+		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: name, Version: version}},
+		Status:     v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: name}}},
+	}
 	pkgi := v1alpha1.PackageInfo{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: names.PackageInfoName(pkg),
-		},
-		Spec: v1alpha1.PackageInfoSpec{
-			Name:    name,
-			Version: version,
-		},
-		Status: v1alpha1.PackageInfoStatus{
-			Version:  version,
-			Manifest: &manifest,
-		},
+		ObjectMeta: metav1.ObjectMeta{Name: names.PackageInfoName(&pkg)},
+		Spec:       v1alpha1.PackageInfoSpec{Name: name, Version: version},
+		Status:     v1alpha1.PackageInfoStatus{Version: version, Manifest: &manifest},
 	}
 	if installed {
 		testClient.packages = append(testClient.packages, pkg)
@@ -113,7 +137,7 @@ var _ = Describe("Dependency Manager", func() {
 
 	BeforeEach(func() {
 		dm = createDependencyManager()
-		p, pi = createPackageAndInfo("P", "12.2.0", false)
+		p, pi = createClusterPackageAndInfo("P", "12.2.0", false)
 	})
 
 	AfterEach(func() {
@@ -128,6 +152,8 @@ var _ = Describe("Dependency Manager", func() {
 		xi = nil
 		y = nil
 		yi = nil
+		d = nil
+		di = nil
 		dm = nil
 	})
 
@@ -157,7 +183,7 @@ var _ = Describe("Dependency Manager", func() {
 				When("D exists", func() {
 
 					BeforeEach(func() {
-						d, di = createPackageAndInfo("D", "1.1.1", true)
+						d, di = createClusterPackageAndInfo("D", "1.1.1", true)
 					})
 
 					When("no other package dependent on D", func() {
@@ -174,11 +200,11 @@ var _ = Describe("Dependency Manager", func() {
 					When("other existing packages X, Y dependent on D", func() {
 
 						BeforeEach(func() {
-							x, xi = createPackageAndInfo("X", "0.17.2", true)
+							x, xi = createClusterPackageAndInfo("X", "0.17.2", true)
 							xi.Status.Manifest.Dependencies = []v1alpha1.Dependency{{
 								Name: "D",
 							}}
-							y, yi = createPackageAndInfo("Y", "3.2.0-beta.7", true)
+							y, yi = createClusterPackageAndInfo("Y", "3.2.0-beta.7", true)
 							yi.Status.Manifest.Dependencies = []v1alpha1.Dependency{{
 								Name: "D",
 							}}
@@ -266,11 +292,11 @@ var _ = Describe("Dependency Manager", func() {
 						When("there is no other existing package dependent on D", func() {
 
 							BeforeEach(func() {
-								x, xi = createPackageAndInfo("X", "0.17.0", true) // X here has no dependency on D
+								x, xi = createClusterPackageAndInfo("X", "0.17.0", true) // X here has no dependency on D
 							})
 
 							It("should return OK if D's version is in required range", func(ctx context.Context) {
-								d, di = createPackageAndInfo("D", "1.3.0", true)
+								d, di = createClusterPackageAndInfo("D", "1.3.0", true)
 								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
@@ -280,7 +306,7 @@ var _ = Describe("Dependency Manager", func() {
 							})
 
 							It("should return CONFLICT if D's version is too old", func(ctx context.Context) {
-								d, di = createPackageAndInfo("D", "1.2.1", true)
+								d, di = createClusterPackageAndInfo("D", "1.2.1", true)
 								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
@@ -295,7 +321,7 @@ var _ = Describe("Dependency Manager", func() {
 							})
 
 							It("should return CONFLICT if D's version is too new", func(ctx context.Context) {
-								d, di = createPackageAndInfo("D", "2.0.0-alpha.2", true)
+								d, di = createClusterPackageAndInfo("D", "2.0.0-alpha.2", true)
 								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
@@ -313,11 +339,11 @@ var _ = Describe("Dependency Manager", func() {
 						When("existing packages X and Y are dependent on D but require no version range of D", func() {
 
 							BeforeEach(func() {
-								x, xi = createPackageAndInfo("X", "0.17.3", true)
+								x, xi = createClusterPackageAndInfo("X", "0.17.3", true)
 								xi.Status.Manifest.Dependencies = []v1alpha1.Dependency{{
 									Name: "D",
 								}}
-								y, yi = createPackageAndInfo("Y", "3.2.0-beta.8", true)
+								y, yi = createClusterPackageAndInfo("Y", "3.2.0-beta.8", true)
 								yi.Status.Manifest.Dependencies = []v1alpha1.Dependency{{
 									Name: "D",
 								}}
@@ -326,7 +352,7 @@ var _ = Describe("Dependency Manager", func() {
 							// these are the same tests as in the previous When("there is no other existing package dependent on D")
 
 							It("should return OK if D's version is in required range", func(ctx context.Context) {
-								d, di = createPackageAndInfo("D", "1.3.1", true)
+								d, di = createClusterPackageAndInfo("D", "1.3.1", true)
 								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
@@ -336,7 +362,7 @@ var _ = Describe("Dependency Manager", func() {
 							})
 
 							It("should return CONFLICT if D's version is too old", func(ctx context.Context) {
-								d, di = createPackageAndInfo("D", "1.1.7", true)
+								d, di = createClusterPackageAndInfo("D", "1.1.7", true)
 								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
@@ -351,7 +377,7 @@ var _ = Describe("Dependency Manager", func() {
 							})
 
 							It("should return CONFLICT if D's version is too new", func(ctx context.Context) {
-								d, di = createPackageAndInfo("D", "2.0.0-alpha.2", true)
+								d, di = createClusterPackageAndInfo("D", "2.0.0-alpha.2", true)
 								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
@@ -370,12 +396,12 @@ var _ = Describe("Dependency Manager", func() {
 					When("other existing packages X, Y are dependent on D and require D in version ranges", func() {
 
 						BeforeEach(func() {
-							x, xi = createPackageAndInfo("X", "0.18.3", true)
+							x, xi = createClusterPackageAndInfo("X", "0.18.3", true)
 							xi.Status.Manifest.Dependencies = []v1alpha1.Dependency{{
 								Name:    "D",
 								Version: "^1.0.0 || 2.0.0",
 							}}
-							y, yi = createPackageAndInfo("X", "3.3.3", true)
+							y, yi = createClusterPackageAndInfo("X", "3.3.3", true)
 							yi.Status.Manifest.Dependencies = []v1alpha1.Dependency{{
 								Name:    "D",
 								Version: ">= 1.1.0, < 3",
@@ -383,7 +409,7 @@ var _ = Describe("Dependency Manager", func() {
 						})
 
 						It("should return OK if D's version is in required range", func(ctx context.Context) {
-							d, di = createPackageAndInfo("D", "1.4.0", true)
+							d, di = createClusterPackageAndInfo("D", "1.4.0", true)
 							res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 							Expect(err).ShouldNot(HaveOccurred())
 							Expect(res).ShouldNot(BeNil())
@@ -393,7 +419,7 @@ var _ = Describe("Dependency Manager", func() {
 						})
 
 						It("should return CONFLICT if D's version is too old", func(ctx context.Context) {
-							d, di = createPackageAndInfo("D", "1.2.1", true)
+							d, di = createClusterPackageAndInfo("D", "1.2.1", true)
 							res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 							Expect(err).ShouldNot(HaveOccurred())
 							Expect(res).ShouldNot(BeNil())
@@ -408,7 +434,7 @@ var _ = Describe("Dependency Manager", func() {
 						})
 
 						It("should return CONFLICT if D's version is too new", func(ctx context.Context) {
-							d, di = createPackageAndInfo("D", "2.0.0", true)
+							d, di = createClusterPackageAndInfo("D", "2.0.0", true)
 							res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 							Expect(err).ShouldNot(HaveOccurred())
 							Expect(res).ShouldNot(BeNil())
@@ -458,8 +484,8 @@ var _ = Describe("Dependency Manager", func() {
 			When("P requires no version ranges of D and E", func() {
 				When("D, E exist", func() {
 					It("Should return OK", func(ctx context.Context) {
-						d, di = createPackageAndInfo("D", "118.0.0", true)
-						e, ei = createPackageAndInfo("E", "11.80.0", true)
+						d, di = createClusterPackageAndInfo("D", "118.0.0", true)
+						e, ei = createClusterPackageAndInfo("E", "11.80.0", true)
 						res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 						Expect(err).ShouldNot(HaveOccurred())
 						Expect(res).ShouldNot(BeNil())
@@ -492,19 +518,43 @@ var _ = Describe("Dependency Manager", func() {
 			})
 
 			When("P requires D and E to be in version ranges", func() {
-
 				// TODO
-
 			})
-
 		})
 
+		When("D is installed", func() {
+			BeforeEach(func() {
+				d, di = createClusterPackageAndInfo("D", "1.1.1", true)
+			})
+			When("there is a namespaced package N that depends on D", func() {
+				BeforeEach(func() {
+					n, ni = createPackageAndInfo("N", "default", "1.0.0", true)
+					ni.Status.Manifest.Dependencies = []v1alpha1.Dependency{{Name: d.Name}}
+				})
+				When("D depends on N with constraint", func() {
+					BeforeEach(func() {
+						ni.Status.Manifest.Dependencies = []v1alpha1.Dependency{{Name: d.Name, Version: "1.x.x"}}
+					})
+					It("should prevent illegal update of D", func(ctx context.Context) {
+						d, di = createClusterPackageAndInfo("D", "2.0.0", false)
+						res, err := dm.Validate(ctx, di.Status.Manifest, di.Spec.Version)
+						Expect(err).ShouldNot(HaveOccurred())
+						Expect(res).ShouldNot(BeNil())
+						Expect(res.Status).Should(Equal(ValidationResultStatusConflict))
+						Expect(res.Requirements).Should(BeEmpty())
+						Expect(res.Conflicts).Should(HaveLen(1))
+					})
+					It("should allow legal update of D", func(ctx context.Context) {
+						d, di = createClusterPackageAndInfo("D", "1.2.0", false)
+						res, err := dm.Validate(ctx, di.Status.Manifest, di.Spec.Version)
+						Expect(err).ShouldNot(HaveOccurred())
+						Expect(res).ShouldNot(BeNil())
+						Expect(res.Status).Should(Equal(ValidationResultStatusOk))
+						Expect(res.Requirements).Should(BeEmpty())
+						Expect(res.Conflicts).Should(BeEmpty())
+					})
+				})
+			})
+		})
 	})
-
-	// TODO test IsUpdateAllowed()
-
-	Describe("CheckConflict", func() {
-
-	})
-
 })
diff --git a/internal/manifestvalues/resolver_test.go b/internal/manifestvalues/resolver_test.go
index 7dd7a75..5d3b115 100644
--- a/internal/manifestvalues/resolver_test.go
+++ b/internal/manifestvalues/resolver_test.go
@@ -75,7 +75,7 @@ var _ = Describe("resolver", func() {
 
 	It("should resolve Package reference value", func(ctx context.Context) {
 		resolver := newTestResolver(
-			&v1alpha1.Package{
+			&v1alpha1.ClusterPackage{
 				ObjectMeta: metav1.ObjectMeta{Name: "test"},
 				Spec: v1alpha1.PackageSpec{
 					Values: map[string]v1alpha1.ValueConfiguration{
diff --git a/internal/webhook/package_validator_test.go b/internal/webhook/package_validator_test.go
index eabf345..c953ecb 100644
--- a/internal/webhook/package_validator_test.go
+++ b/internal/webhook/package_validator_test.go
@@ -8,6 +8,7 @@ import (
 	ctrladapter "github.com/glasskube/glasskube/internal/adapter/controllerruntime"
 	"github.com/glasskube/glasskube/internal/controller/owners"
 	"github.com/glasskube/glasskube/internal/dependency"
+	"github.com/glasskube/glasskube/internal/names"
 	fakerepo "github.com/glasskube/glasskube/internal/repo/client/fake"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -42,59 +43,67 @@ func newPackageValidatingWebhook(objects ...client.Object) *PackageValidatingWeb
 }
 
 var (
-	foov1pkg = v1alpha1.Package{
+	foov1pkg = v1alpha1.ClusterPackage{
 		ObjectMeta: v1.ObjectMeta{Name: "foo"},
 		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "foo", Version: "v1"}},
 		Status:     v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: "foo--v1"}}}}
 	foov1pi = v1alpha1.PackageInfo{
-		ObjectMeta: v1.ObjectMeta{Name: "foo--v1"},
+		ObjectMeta: v1.ObjectMeta{Name: names.PackageInfoName(&foov1pkg)},
 		Spec:       v1alpha1.PackageInfoSpec{Name: "foo", Version: "v1"},
 		Status: v1alpha1.PackageInfoStatus{
 			Manifest: &v1alpha1.PackageManifest{Name: "foo", Dependencies: []v1alpha1.Dependency{{Name: "bar", Version: "v1"}}}}}
-	foov2pkg = v1alpha1.Package{
+	foov2pkg = v1alpha1.ClusterPackage{
 		ObjectMeta: v1.ObjectMeta{Name: "foo"},
 		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "foo", Version: "v2"}},
 		Status:     v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: "foo--v2"}}}}
 	foov2pi = v1alpha1.PackageInfo{
-		ObjectMeta: v1.ObjectMeta{Name: "foo--v2"},
+		ObjectMeta: v1.ObjectMeta{Name: names.PackageInfoName(&foov2pkg)},
 		Spec:       v1alpha1.PackageInfoSpec{Name: "foo", Version: "v2"},
 		Status: v1alpha1.PackageInfoStatus{
 			Manifest: &v1alpha1.PackageManifest{Name: "foo", Dependencies: []v1alpha1.Dependency{{Name: "bar", Version: "v2"}}}}}
-	barv1pkg = v1alpha1.Package{
+	barv1pkg = v1alpha1.ClusterPackage{
 		ObjectMeta: v1.ObjectMeta{Name: "bar"},
 		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "bar", Version: "v1"}},
 		Status:     v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: "bar--v1"}}}}
 	barv1pi = v1alpha1.PackageInfo{
-		ObjectMeta: v1.ObjectMeta{Name: "bar--v1"},
+		ObjectMeta: v1.ObjectMeta{Name: names.PackageInfoName(&barv1pkg)},
 		Spec:       v1alpha1.PackageInfoSpec{Name: "bar", Version: "v1"},
 		Status:     v1alpha1.PackageInfoStatus{Manifest: &v1alpha1.PackageManifest{Name: "bar"}}}
-	barv2pkg = v1alpha1.Package{
+	barv2pkg = v1alpha1.ClusterPackage{
 		ObjectMeta: v1.ObjectMeta{Name: "bar"},
 		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "bar", Version: "v2"}},
 		Status:     v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: "bar--v2"}}}}
 	barv2pi = v1alpha1.PackageInfo{
-		ObjectMeta: v1.ObjectMeta{Name: "bar--v2"},
+		ObjectMeta: v1.ObjectMeta{Name: names.PackageInfoName(&barv2pkg)},
 		Spec:       v1alpha1.PackageInfoSpec{Name: "bar", Version: "v2"},
 		Status:     v1alpha1.PackageInfoStatus{Manifest: &v1alpha1.PackageManifest{Name: "bar"}}}
-	bazv1pkg = v1alpha1.Package{
+	bazv1pkg = v1alpha1.ClusterPackage{
 		ObjectMeta: v1.ObjectMeta{Name: "baz"},
 		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "baz", Version: "v1"}},
 		Status:     v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: "baz--v1"}}}}
 	bazv1pi = v1alpha1.PackageInfo{
-		ObjectMeta: v1.ObjectMeta{Name: "baz--v1"},
+		ObjectMeta: v1.ObjectMeta{Name: names.PackageInfoName(&bazv1pkg)},
 		Spec:       v1alpha1.PackageInfoSpec{Name: "baz", Version: "v1"},
 		Status: v1alpha1.PackageInfoStatus{
 			Manifest: &v1alpha1.PackageManifest{Name: "baz", Dependencies: []v1alpha1.Dependency{{Name: "foo", Version: "v1"}}}}}
-	bazv2pkg = v1alpha1.Package{
+	bazv2pkg = v1alpha1.ClusterPackage{
 		ObjectMeta: v1.ObjectMeta{Name: "baz"},
 		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "baz", Version: "v2"}},
 		Status:     v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: "baz--v2"}}}}
 	bazv2pi = v1alpha1.PackageInfo{
-		ObjectMeta: v1.ObjectMeta{Name: "baz--v1"},
+		ObjectMeta: v1.ObjectMeta{Name: names.PackageInfoName(&bazv2pkg)},
 		Spec:       v1alpha1.PackageInfoSpec{Name: "baz", Version: "v1"},
 		Status: v1alpha1.PackageInfoStatus{
 			Manifest: &v1alpha1.PackageManifest{Name: "baz", Dependencies: []v1alpha1.Dependency{{Name: "foo", Version: "v2"}}}}}
-	notExistsPkg = v1alpha1.Package{
+	nspv1pkg = v1alpha1.Package{
+		ObjectMeta: v1.ObjectMeta{Name: "nsp", Namespace: "default"},
+		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "nsp", Version: "v1"}}}
+	nspv1pi = v1alpha1.PackageInfo{
+		ObjectMeta: v1.ObjectMeta{Name: names.PackageInfoName(&nspv1pkg)},
+		Spec:       v1alpha1.PackageInfoSpec{Name: "nsp", Version: "v1"},
+		Status: v1alpha1.PackageInfoStatus{
+			Manifest: &v1alpha1.PackageManifest{Name: "nsp", Dependencies: []v1alpha1.Dependency{{Name: "foo", Version: "v1"}}}}}
+	notExistsPkg = v1alpha1.ClusterPackage{
 		ObjectMeta: v1.ObjectMeta{Name: "doesnotexist"},
 		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "doesnotexist", Version: "v1"}}}
 )
@@ -121,6 +130,9 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 				"v1": bazv1pi.Status.Manifest,
 				"v2": bazv2pi.Status.Manifest,
 			},
+			"nsp": {
+				"v1": nspv1pi.Status.Manifest,
+			},
 		}
 	})
 
@@ -184,8 +196,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 			When("dependency version ok", func() {
 				It("should not return error", func(ctx context.Context) {
 					webhook := newPackageValidatingWebhook(&barv2pkg, &barv2pi)
-					_, err := webhook.ValidateUpdate(ctx,
-						&foov1pkg, &foov2pkg)
+					_, err := webhook.ValidateUpdate(ctx, &foov1pkg, &foov2pkg)
 					Expect(err).NotTo(HaveOccurred())
 				})
 			})
@@ -211,6 +222,13 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 				Expect(err).To(HaveOccurred())
 			})
 		})
+		When("a namespaced dependent exists with constraint", func() {
+			It("should return error", func(ctx context.Context) {
+				webhook := newPackageValidatingWebhook(&nspv1pkg, &nspv1pi, &foov1pkg, &foov1pi)
+				_, err := webhook.ValidateUpdate(ctx, &foov1pkg, &foov2pkg)
+				Expect(err).To(HaveOccurred())
+			})
+		})
 	})
 
 	Context("ValidateDelete", func() {