Repository: glasskube/glasskube

Issue:
Offer only allowed updates on UI/CLI
**Is your feature request related to a problem? Please describe.**
As of now, we show the update button (and the "vX.Y.Z available" in the CLI) whenever the latestVersion is not the installed version. The first problem with this is, that the user might be on a more current version than latest (see #329). The second one is, that it might not be possible to update to the latest version, because the package is required as a dependency by another package, or because a dependent package cannot be updated. Maybe there are even more cases where an update is not possible – TBD.

**Describe the solution you'd like**
UI/CLI should only offer the user to only show the updates that are possible in their cluster. This means that a package might not be able to be updated to latest version but to a version below that, or maybe not be able to be updated at all.

Parts of this have been done in #401 – but right now we don't offer the latest possible version to update to, we just don't offer an update at all.

**Describe alternatives you've considered**
Not implementing this and instead showing an error to the user that the update is not possible. This would still be possible but be a bit confusing and inconsistent. Also, it would not be possible to update to a lower version than latest. 

**Additional context**
Kind of related in terms of same code that will be touched: #329 
Prior work on this: #401


Hints from issue comments:
@hanshal101: Hey @christophenne I would like to work on this. Can you give me some more information on this.@christophenne: Hi @hanshal101, thanks a lot, but we have to talk about this one internally first, I think it's better to pick another issue – as always the ones labelled "help wanted" are a good choice. If you are still interested in #441 for example, and if you don't have any other issues assigned right now (except for the one where the PR is open), I think that would be a good issue to look at now.@prikshitsingh24: @christophenne could i work on this or is it still not available ?@christophenne: @prikshitsingh24 good question, the issue description is a bit outdated, but not completely obsolete. 
On the UI (not released yet) the user can now select every version and see if it has dependency conflicts or not (However the recommended action "Set Latest" can of course lead the user to a conflicting version). On the CLI the user can update to a specific version with the `-v` flag. 
I think it would be fair to change the update command semantics to update to the latest possible (i.e. check latest first, if it doesn't work, check the (latest-1)th version, and so on...), if not automatically, then via a flag? This would probably also be fair for auto-update users, because they rely on automatic updates but don't get smaller/minor updates anymore as soon as a newer major latest version breaks their dependency graph. @pmig @kosmoz 

Solution patch:
diff --git a/README.md b/README.md
index f709f66..09676f5 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
   </a>
   <img referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=899d5aee-625c-4345-bad0-713d29caf929" />
 
-<h3 align="center">🧊 The missing Package Manager for Kubernetes 📦</h3>
+<h3 align="center">🧊 The next generation Package Manager for Kubernetes 📦</h3>
 
   <p align="center">
     <a href="https://glasskube.dev/docs/getting-started/install?utm_source=github"><strong>Getting started »</strong></a>
@@ -107,7 +107,7 @@ Congratulations, you can now explore and install all our available packages! 
 
 ### Coming Soon
 
-In the next versions you will be able to install more packages like the [Kube-Prometheus-Stack](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack), [Velero](https://github.com/vmware-tanzu/velero) and the [CloudNativePG](https://github.com/cloudnative-pg/cloudnative-pg). With v0.1.0 you will also be able to install apps like [GitLab](https://gitlab.com/gitlab-org/gitlab), [Keycloak](https://github.com/keycloak/keycloak) or [Hashicorp Vault](https://github.com/hashicorp/vault) with the [Glasskube Apps Operator](https://github.com/glasskube/operator/). You can find the full list and release schedule in our [roadmap](https://glasskube.dev/roadmap/#packages-and-apps-schedule?utm_source=github)
+In the next versions you will be able to install more packages like the [Kube-Prometheus-Stack](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack), [Velero](https://github.com/vmware-tanzu/velero) and the [CloudNativePG](https://github.com/cloudnative-pg/cloudnative-pg). With v1.0.0 you will also be able to install apps like [GitLab](https://gitlab.com/gitlab-org/gitlab), [Keycloak](https://github.com/keycloak/keycloak) or [Hashicorp Vault](https://github.com/hashicorp/vault) with the [Glasskube Apps Operator](https://github.com/glasskube/operator/). You can find the full list and release schedule in our [roadmap](https://glasskube.dev/roadmap/#packages-and-apps-schedule?utm_source=github)
 
 > Can't find a package or want your app included in the list? We are always adding new supported packages & apps,
 > so just join us on [Discord](https://discord.gg/SxH6KUCGH7) or open up a new issue and let us know what is missing!
@@ -163,6 +163,10 @@ Thanks to everyone, that is supporting this project. We are thankful, for evey c
   <img src="https://contrib.rocks/image?repo=glasskube/glasskube" />
 </a>
 
+## 👾 Activity
+
+![Glasskube Activity](https://repobeats.axiom.co/api/embed/c5aac6f5d22bd6b83a21ae51353dd7bcb43f9517.svg "Glasskube activity image")
+
 ## 📘 License
 
 The Glasskube is licensed under the Apache 2.0 license. For more information check the [LICENSE](https://github.com/glasskube/glasskube/blob/main/LICENSE) file for details.
diff --git a/cmd/glasskube/cmd/install.go b/cmd/glasskube/cmd/install.go
index 98146e4..0d88b6d 100644
--- a/cmd/glasskube/cmd/install.go
+++ b/cmd/glasskube/cmd/install.go
@@ -55,14 +55,16 @@ var installCmd = &cobra.Command{
 				installCmdOptions.Version, packageName)
 		}
 
-		installationPlan := []dependency.PackageWithVersion{{Name: packageName, Version: installCmdOptions.Version}}
+		installationPlan := []dependency.Requirement{
+			{PackageWithVersion: dependency.PackageWithVersion{Name: packageName, Version: installCmdOptions.Version}},
+		}
 
 		var manifest v1alpha1.PackageManifest
 		if err := repo.FetchPackageManifest("", packageName, installCmdOptions.Version, &manifest); err != nil {
 			fmt.Fprintf(os.Stderr, "❗ Error: Could not fetch package manifest: %v\n", err)
 			os.Exit(1)
 		} else if validationResult, err :=
-			dm.Validate(ctx, &manifest); err != nil {
+			dm.Validate(ctx, &manifest, installCmdOptions.Version); err != nil {
 			fmt.Fprintf(os.Stderr, "❗ Error: Could not validate dependencies: %v\n", err)
 			os.Exit(1)
 		} else if len(validationResult.Conflicts) > 0 {
diff --git a/internal/controller/package_controller.go b/internal/controller/package_controller.go
index b3c2d0b..42c2a2e 100644
--- a/internal/controller/package_controller.go
+++ b/internal/controller/package_controller.go
@@ -22,6 +22,7 @@ import (
 	"strings"
 
 	"github.com/glasskube/glasskube/internal/dependency/adapter/controllerruntime"
+	"github.com/glasskube/glasskube/internal/names"
 	"k8s.io/apimachinery/pkg/types"
 
 	"github.com/glasskube/glasskube/internal/dependency"
@@ -230,13 +231,17 @@ func (r *PackageReconcilationContext) ensureDependencies(ctx context.Context) bo
 	log.V(1).Info("ensuring dependencies", "dependencies", r.pi.Status.Manifest.Dependencies)
 
 	var failed []string
-	if result, err := r.dependencyMgr.Validate(ctx, r.pi.Status.Manifest); err != nil {
+	if result, err := r.dependencyMgr.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,
@@ -343,7 +348,7 @@ func (r *PackageReconcilationContext) ensureDependencies(ctx context.Context) bo
 
 func (r *PackageReconcilationContext) ensurePackageInfo(ctx context.Context) error {
 	packageInfo := packagesv1alpha1.PackageInfo{
-		ObjectMeta: metav1.ObjectMeta{Name: generatePackageInfoName(*r.pkg)},
+		ObjectMeta: metav1.ObjectMeta{Name: names.PackageInfoName(*r.pkg)},
 	}
 	log := ctrl.LoggerFrom(ctx).WithValues("PackageInfo", packageInfo.Name)
 
@@ -387,10 +392,6 @@ func (r *PackageReconcilationContext) ensurePackageInfo(ctx context.Context) err
 	return nil
 }
 
-func generatePackageInfoName(pkg packagesv1alpha1.Package) string {
-	return escapeResourceName(fmt.Sprintf("%v--%v", pkg.Spec.PackageInfo.Name, pkg.Spec.PackageInfo.Version))
-}
-
 func (r *PackageReconcilationContext) handleAdapterResults(ctx context.Context, results []result.ReconcileResult) bool {
 	var firstFailed *result.ReconcileResult
 	var firstWaiting *result.ReconcileResult
diff --git a/internal/dependency/adapter/adapter.go b/internal/dependency/adapter/adapter.go
index 4f1002c..2d7eb5e 100644
--- a/internal/dependency/adapter/adapter.go
+++ b/internal/dependency/adapter/adapter.go
@@ -7,12 +7,11 @@ import (
 )
 
 type ClientAdapter interface {
-	GetPackage(ctx context.Context, pkgName string) (*v1alpha1.Package, error)
 	GetPackageInfo(ctx context.Context, pkgInfoName string) (*v1alpha1.PackageInfo, error)
 	ListPackages(ctx context.Context) (*v1alpha1.PackageList, error)
 }
 
 type RepoAdapter interface {
-	GetLatestVersion(repo string, pkgName string) (string, error)
-	GetMaxVersionCompatibleWith(repo string, pkgName string, versionRange string) (string, error)
+	GetVersions(repoURL string, name string) ([]string, error)
+	GetManifest(repoURL string, name string, version string) (*v1alpha1.PackageManifest, error)
 }
diff --git a/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go b/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go
index 6e2e048..9e68ba2 100644
--- a/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go
+++ b/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go
@@ -3,8 +3,6 @@ package controllerruntime
 import (
 	"context"
 
-	"k8s.io/apimachinery/pkg/api/errors"
-
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"k8s.io/apimachinery/pkg/types"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -20,21 +18,6 @@ func NewControllerRuntimeAdapter(client client.Client) *ControllerRuntimeAdapter
 	}
 }
 
-func (a *ControllerRuntimeAdapter) GetPackage(ctx context.Context, pkgName string) (*v1alpha1.Package, error) {
-	var pkg v1alpha1.Package
-	if err := a.client.Get(ctx, types.NamespacedName{
-		Name: pkgName,
-	}, &pkg); err != nil {
-		if errors.IsNotFound(err) {
-			return nil, nil
-		} else {
-			return nil, err
-		}
-	} else {
-		return &pkg, nil
-	}
-}
-
 func (a *ControllerRuntimeAdapter) GetPackageInfo(ctx context.Context, pkgInfoName string) (*v1alpha1.PackageInfo, error) {
 	var pkgInfo v1alpha1.PackageInfo
 	if err := a.client.Get(ctx, types.NamespacedName{
diff --git a/internal/dependency/adapter/goclient/goclientadapter.go b/internal/dependency/adapter/goclient/goclientadapter.go
index 48d6739..4cad211 100644
--- a/internal/dependency/adapter/goclient/goclientadapter.go
+++ b/internal/dependency/adapter/goclient/goclientadapter.go
@@ -5,7 +5,6 @@ import (
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/pkg/client"
-	"k8s.io/apimachinery/pkg/api/errors"
 )
 
 type GoClientAdapter struct {
@@ -18,19 +17,6 @@ func NewGoClientAdapter(pkgClient client.PackageV1Alpha1Client) *GoClientAdapter
 	}
 }
 
-func (a *GoClientAdapter) GetPackage(ctx context.Context, pkgName string) (*v1alpha1.Package, error) {
-	var pkg v1alpha1.Package
-	if err := a.pkgClient.Packages().Get(ctx, pkgName, &pkg); err != nil {
-		if errors.IsNotFound(err) {
-			return nil, nil
-		} else {
-			return nil, err
-		}
-	} else {
-		return &pkg, nil
-	}
-}
-
 func (a *GoClientAdapter) GetPackageInfo(ctx context.Context, pkgInfoName string) (*v1alpha1.PackageInfo, error) {
 	var pkgInfo v1alpha1.PackageInfo
 	if err := a.pkgClient.PackageInfos().Get(ctx, pkgInfoName, &pkgInfo); err != nil {
diff --git a/internal/dependency/default_repo_adapter.go b/internal/dependency/default_repo_adapter.go
new file mode 100644
index 0000000..f2eadea
--- /dev/null
+++ b/internal/dependency/default_repo_adapter.go
@@ -0,0 +1,28 @@
+package dependency
+
+import (
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/repo"
+	"github.com/glasskube/glasskube/internal/repo/client"
+)
+
+type defaultRepoAdapter struct {
+	repo client.RepoClient
+}
+
+func (a *defaultRepoAdapter) GetVersions(repoURL string, name string) ([]string, error) {
+	var idx repo.PackageIndex
+	if err := a.repo.FetchPackageIndex(repoURL, name, &idx); err != nil {
+		return nil, err
+	}
+	versions := make([]string, len(idx.Versions))
+	for i, item := range idx.Versions {
+		versions[i] = item.Version
+	}
+	return versions, nil
+}
+
+func (a *defaultRepoAdapter) GetManifest(repoURL string, name string, version string) (*v1alpha1.PackageManifest, error) {
+	var manifest v1alpha1.PackageManifest
+	return &manifest, a.repo.FetchPackageManifest(repoURL, name, version, &manifest)
+}
diff --git a/internal/dependency/graph/errors.go b/internal/dependency/graph/errors.go
new file mode 100644
index 0000000..b1e6033
--- /dev/null
+++ b/internal/dependency/graph/errors.go
@@ -0,0 +1,61 @@
+package graph
+
+import (
+	"fmt"
+
+	"github.com/Masterminds/semver/v3"
+)
+
+func ErrNotInstalled(name string) error {
+	err := NotInstalledError(name)
+	return &err
+}
+
+// NotInstalledError indicates that a package is missing
+type NotInstalledError string
+
+func (err *NotInstalledError) Error() string {
+	return fmt.Sprintf("%v not installed", string(*err))
+}
+
+func ErrConstraint(name string, version *semver.Version, constraint *semver.Constraints, cause error) error {
+	return &ConstraintError{Name: name, Version: version, Constraint: constraint, cause: cause}
+}
+
+// ConstraintError indicates that a constraint has been violated
+type ConstraintError struct {
+	Name       string
+	Version    *semver.Version
+	Constraint *semver.Constraints
+	cause      error
+}
+
+func (err *ConstraintError) Error() string {
+	return fmt.Sprintf("constraint %v violated: %v", err.Constraint, err.cause)
+}
+
+func ErrDependency(name, dep string, cause error) error {
+	return &DependencyError{Name: name, Dependency: dep, cause: cause}
+}
+
+// DependencyError idicates that a dependency is not met
+type DependencyError struct {
+	Name, Dependency string
+	cause            error
+}
+
+func (err *DependencyError) Error() string {
+	return fmt.Sprintf("unmet dependency %v -> %v: %v", err.Name, err.Dependency, err.cause)
+}
+
+func (err *DependencyError) Is(other error) bool {
+	_, ok := other.(*DependencyError)
+	return ok
+}
+
+func (err *DependencyError) Unwrap() error {
+	return err.cause
+}
+
+var _ error = &ConstraintError{}
+var _ error = &DependencyError{}
diff --git a/internal/dependency/graph/graph.go b/internal/dependency/graph/graph.go
new file mode 100644
index 0000000..8fd301b
--- /dev/null
+++ b/internal/dependency/graph/graph.go
@@ -0,0 +1,250 @@
+package graph
+
+import (
+	"fmt"
+
+	"github.com/Masterminds/semver/v3"
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	isemver "github.com/glasskube/glasskube/internal/semver"
+	"go.uber.org/multierr"
+)
+
+type DependencyGraph struct {
+	vertices map[string]*vertex
+}
+
+type vertex struct {
+	version *semver.Version
+	manual  bool
+	edges   map[string]*edge
+}
+
+type edge struct {
+	constraint *semver.Constraints
+	vertex     *vertex
+}
+
+func NewGraph() *DependencyGraph {
+	return &DependencyGraph{vertices: map[string]*vertex{}}
+}
+
+// Add simulates installing or updating a package 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)
+	return err
+}
+
+// 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 {
+		return vertex.manual
+	} else {
+		return false
+	}
+}
+
+// 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 {
+		return vertex.version
+	} else {
+		return nil
+	}
+}
+
+// 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 {
+		dependencies := make([]string, len(vertex.edges))
+		i := 0
+		for dep := range vertex.edges {
+			dependencies[i] = dep
+			i++
+		}
+		return dependencies
+	} else {
+		return nil
+	}
+}
+
+// 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 {
+		if _, ok := vertex.edges[of]; vertex.version != nil && ok {
+			dependants = append(dependants, name)
+		}
+	}
+	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 {
+		if edge, ok := vertex.edges[of]; ok && vertex.version != nil && edge.constraint != nil {
+			constraints = append(constraints, edge.constraint)
+		}
+	}
+	return constraints
+}
+
+// Max returns the maximum element of versions that does not violate any constraint of this package
+func (g *DependencyGraph) Max(of string, versions []*semver.Version) (*semver.Version, error) {
+	var maxVersion *semver.Version
+outer:
+	for _, version := range versions {
+		if maxVersion == nil || maxVersion.LessThan(version) {
+			for _, constraint := range g.Constraints(of) {
+				if isemver.ValidateVersionConstraint(version, constraint) != nil {
+					continue outer
+				}
+			}
+			maxVersion = version
+		}
+	}
+	if maxVersion != nil {
+		return maxVersion, nil
+	} else {
+		return nil, fmt.Errorf("no matching version for %v found", of)
+	}
+}
+
+// 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 newGraph
+}
+
+// Delete simulates uninstalling a package.
+//
+// The vertex is not actually removed from the graph, as it may still be referenced by other
+// 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)
+}
+
+// Prune deletes all vertices for which all of the following applies:
+// 1. It has not been installed manually
+// 2. It does not have any dependants
+func (g *DependencyGraph) Prune() []string {
+	stable := false
+	var removed []string
+	for !stable {
+		stable = true
+		for name, vertex := range g.vertices {
+			if !vertex.manual && len(g.Dependants(name)) == 0 && g.delete(name) {
+				stable = false
+				removed = append(removed, name)
+			}
+		}
+	}
+	return removed
+}
+
+func (g *DependencyGraph) DeleteAndPrune(name string) []string {
+	if g.delete(name) {
+		return append([]string{name}, g.Prune()...)
+	}
+	return nil
+}
+
+func (g *DependencyGraph) ValidateDelete(name string) ([]string, error) {
+	gc := g.DeepCopy()
+	return gc.DeleteAndPrune(name), gc.Validate()
+}
+
+// Validate checks the consistency of the entire graph by checking that
+// 1. All vertices with at least one dependency have a version that is not nil
+// 2. There are no violated version constraints
+func (g *DependencyGraph) Validate() error {
+	var err error
+	for name, vertex := range g.vertices {
+		for dep, edge := range vertex.edges {
+			if edge.vertex.version == nil {
+				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)))
+				}
+			}
+		}
+	}
+	return err
+}
+
+func (g *DependencyGraph) vertex(name string) *vertex {
+	if n, ok := g.vertices[name]; ok {
+		return n
+	} else {
+		return g.createVertex(name, nil)
+	}
+}
+
+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) edge(from string, to string, constraint *semver.Constraints) {
+	g.vertex(from).edges[to] = &edge{
+		vertex:     g.vertex(to),
+		constraint: constraint,
+	}
+}
+
+func (g *DependencyGraph) add(name, version string, dependencies []v1alpha1.Dependency, manual bool) (*vertex, error) {
+	if version == "" {
+		g.delete(name)
+		return g.vertex(name), nil
+	}
+
+	parsedVersion, err := semver.NewVersion(version)
+	if err != nil {
+		return nil, err
+	}
+
+	vertex := g.vertex(name)
+	vertex.version = parsedVersion
+	vertex.manual = manual
+	vertex.edges = map[string]*edge{}
+
+	for _, dep := range dependencies {
+		var constraint *semver.Constraints
+		if len(dep.Version) > 0 {
+			if c, err := semver.NewConstraint(dep.Version); err != nil {
+				return nil, err
+			} else {
+				constraint = c
+			}
+		}
+		g.edge(name, dep.Name, constraint)
+	}
+
+	return vertex, nil
+}
+
+func (g *DependencyGraph) delete(name string) bool {
+	vertex := g.vertex(name)
+	deleted := vertex.version != nil
+	vertex.version = nil
+	vertex.manual = false
+	vertex.edges = map[string]*edge{}
+	return deleted
+}
diff --git a/internal/dependency/manager.go b/internal/dependency/manager.go
index 561dce5..f5d975d 100644
--- a/internal/dependency/manager.go
+++ b/internal/dependency/manager.go
@@ -3,19 +3,20 @@ package dependency
 import (
 	"context"
 	"errors"
-	"fmt"
-	"sort"
+	"slices"
 	"strings"
 
 	"github.com/glasskube/glasskube/internal/controller/owners"
-	"github.com/glasskube/glasskube/internal/controller/owners/utils"
+	"github.com/glasskube/glasskube/internal/names"
+	"go.uber.org/multierr"
 
 	"github.com/glasskube/glasskube/internal/dependency/adapter"
+	"github.com/glasskube/glasskube/internal/dependency/graph"
 
 	"github.com/Masterminds/semver/v3"
 	"github.com/glasskube/glasskube/api/v1alpha1"
-	repo2 "github.com/glasskube/glasskube/internal/repo"
-	"github.com/glasskube/glasskube/internal/repo/client"
+	"github.com/glasskube/glasskube/internal/repo"
+	repoclient "github.com/glasskube/glasskube/internal/repo/client"
 )
 
 type DependendcyManager struct {
@@ -24,120 +25,56 @@ type DependendcyManager struct {
 	*owners.OwnerManager
 }
 
-type ValidationResultStatus string
-
-const (
-	ValidationResultStatusOk         ValidationResultStatus = "OK"
-	ValidationResultStatusResolvable ValidationResultStatus = "RESOLVABLE"
-	ValidationResultStatusConflict   ValidationResultStatus = "CONFLICT"
-)
-
-type PackageWithVersion struct {
-	Name    string
-	Version string
-}
-
-type Requirement struct {
-	PackageWithVersion
-}
-
-type Conflict struct {
-	Actual   PackageWithVersion
-	Required PackageWithVersion
-}
-
-func (cf Conflict) String() string {
-	return fmt.Sprintf("%v (required: %v, actual: %v)", cf.Required.Name, cf.Required.Version, cf.Actual.Version)
-}
-
-type Conflicts []Conflict
-
-func (cf Conflicts) String() string {
-	s := make([]string, len(cf))
-	for i, c := range cf {
-		s[i] = c.String()
+func NewDependencyManager(adapter adapter.ClientAdapter, ownerMgr *owners.OwnerManager) *DependendcyManager {
+	return &DependendcyManager{
+		clientAdapter: adapter,
+		repoAdapter:   &defaultRepoAdapter{repo: repo.DefaultClient},
+		OwnerManager:  ownerMgr,
 	}
-	return strings.Join(s, ", ")
-}
-
-type ValidationResult struct {
-	Status       ValidationResultStatus
-	Requirements []PackageWithVersion
-	Conflicts    Conflicts
-}
-
-type defaultRepoAdapter struct {
-	repo client.RepoClient
 }
 
-func (a *defaultRepoAdapter) GetLatestVersion(repo string, pkgName string) (string, error) {
-	return a.repo.GetLatestVersion(repo, pkgName)
+func (dm *DependendcyManager) WithRepo(repoClient repoclient.RepoClient) *DependendcyManager {
+	dm.repoAdapter = &defaultRepoAdapter{repo: repoClient}
+	return dm
 }
 
-func (a *defaultRepoAdapter) GetMaxVersionCompatibleWith(repo string, pkgName string, versionRange string) (string, error) {
-	var idx repo2.PackageIndex
-	if err := a.repo.FetchPackageIndex(repo, pkgName, &idx); err != nil {
-		return "", err
+func (dm *DependendcyManager) Validate(
+	ctx context.Context,
+	manifest *v1alpha1.PackageManifest,
+	version string,
+) (*ValidationResult, error) {
+	if manifest == nil {
+		return nil, errors.New("manifest must not be nil")
 	}
-	constraint, err := semver.NewConstraint(versionRange)
+
+	g, err := dm.NewGraph(ctx)
 	if err != nil {
-		return "", err
-	}
-	var compatibleVersions []*semver.Version
-	for _, v := range idx.Versions {
-		if version, err := semver.NewVersion(v.Version); err != nil {
-			continue
-		} else if ok := constraint.Check(version); ok {
-			compatibleVersions = append(compatibleVersions, version)
-		}
-	}
-	if len(compatibleVersions) > 0 {
-		collection := semver.Collection(compatibleVersions)
-		sort.Sort(collection)
-		return collection[len(collection)-1].Original(), nil
-	} else {
-		return "", errors.New("no compatible versions found")
+		return nil, err
 	}
-}
 
-func NewDependencyManager(adapter adapter.ClientAdapter, ownerMgr *owners.OwnerManager) *DependendcyManager {
-	return &DependendcyManager{
-		clientAdapter: adapter,
-		repoAdapter:   &defaultRepoAdapter{repo: repo2.DefaultClient},
-		OwnerManager:  ownerMgr,
-	}
-}
+	// We can not call validate the graph here, because the initial graph, representing the current cluster state, may
+	// actually be invalid. This is because we let the operator create/update dependencies which can only happen after
+	// a package is already created.
 
-func (dm *DependendcyManager) WithRepo(repo client.RepoClient) *DependendcyManager {
-	dm.repoAdapter = &defaultRepoAdapter{repo: repo}
-	return dm
-}
+	if err := dm.add(g, *manifest, version); err != nil {
+		return nil, err
+	}
 
-func (dm *DependendcyManager) Validate(ctx context.Context, piManifest *v1alpha1.PackageManifest) (*ValidationResult, error) {
-	if piManifest == nil {
-		return nil, errors.New("nil not allowed")
+	requirements, err := dm.addDependencies(g, manifest.Name, false)
+	if err != nil {
+		return nil, err
 	}
-	var requirements []PackageWithVersion
-	var conflicts []Conflict
+	slices.SortFunc(requirements, func(a, b Requirement) int { return strings.Compare(a.Name, b.Name) })
 
-	// TODO this should be parallelized (be aware that tests will fail because they expect sorted requirements/conflicts)
-	for _, dependency := range piManifest.Dependencies {
-		if requiredPkg, err := dm.clientAdapter.GetPackage(ctx, dependency.Name); err != nil {
+	var conflicts []Conflict
+	for _, err := range multierr.Errors(g.Validate()) {
+		if conflict, err := errorToConflict(err); err != nil {
 			return nil, err
-		} else if requiredPkg == nil {
-			if req, err := dm.createRequirement(dependency); err != nil {
-				return nil, err
-			} else {
-				requirements = append(requirements, *req)
-			}
-		} else if dependency.Version != "" {
-			if conflict, err := dm.CheckConflict(requiredPkg.Spec.PackageInfo.Version, dependency); err != nil {
-				return nil, err
-			} else if conflict != nil {
-				conflicts = append(conflicts, *conflict)
-			}
+		} else {
+			conflicts = append(conflicts, *conflict)
 		}
 	}
+
 	status := ValidationResultStatusOk
 	if len(requirements) > 0 {
 		status = ValidationResultStatusResolvable
@@ -152,147 +89,107 @@ func (dm *DependendcyManager) Validate(ctx context.Context, piManifest *v1alpha1
 	}, nil
 }
 
-func (dm *DependendcyManager) CheckConflict(existingVersionStr string, dependency v1alpha1.Dependency) (*Conflict, error) {
-	existingVersion, err := semver.NewVersion(existingVersionStr)
-	if err != nil {
-		return nil, err
-	}
-	requiredRange, err := semver.NewConstraint(dependency.Version)
+// NewGraph constructs a DependencyGraph from all packages returned by clientAdapter.ListPackages
+func (dm *DependendcyManager) NewGraph(ctx context.Context) (*graph.DependencyGraph, error) {
+	pkgs, err := dm.clientAdapter.ListPackages(ctx)
 	if err != nil {
 		return nil, err
 	}
-	if ok := requiredRange.Check(existingVersion); !ok {
-		return &Conflict{
-			Actual: PackageWithVersion{
-				Name:    dependency.Name,
-				Version: existingVersionStr,
-			},
-			Required: PackageWithVersion{
-				Name:    dependency.Name,
-				Version: dependency.Version,
-			},
-		}, nil
-	} else {
-		return nil, nil
-	}
-}
-
-func (dm *DependendcyManager) createRequirement(dependency v1alpha1.Dependency) (*PackageWithVersion, error) {
-	requirement := &PackageWithVersion{
-		Name: dependency.Name,
-	}
-	if dependency.Version == "" {
-		if latest, err := dm.repoAdapter.GetLatestVersion("", dependency.Name); err != nil {
+	g := graph.NewGraph()
+	for _, pkg := range pkgs.Items {
+		var deps []v1alpha1.Dependency
+		installedVersion := pkg.Spec.PackageInfo.Version
+		if !pkg.DeletionTimestamp.IsZero() {
+			// A package that is currently being deleted is added to the graph, but in a state representing
+			// "not installed"
+			installedVersion = ""
+		} else if pi, err := dm.clientAdapter.GetPackageInfo(ctx, names.PackageInfoName(pkg)); err != nil {
 			return nil, err
-		} else {
-			requirement.Version = latest
-		}
-	} else {
-		if maxCompatible, err := dm.repoAdapter.GetMaxVersionCompatibleWith("", dependency.Name, dependency.Version); err != nil {
-			return nil, err
-		} else {
-			requirement.Version = maxCompatible
+		} else if pi.Status.Manifest != nil {
+			deps = pi.Status.Manifest.Dependencies
 		}
-	}
-	return requirement, nil
-}
-
-type dependentTuple struct {
-	Pkg        *v1alpha1.Package
-	Dependency v1alpha1.Dependency
-}
-
-func (dm *DependendcyManager) IsUpdateAllowed(ctx context.Context, pkg *v1alpha1.Package, version string) (Conflicts, error) {
-	dependents, err := dm.GetDependents(ctx, pkg, GetDependentsOptions{IncludeDependencies: true})
-	if err != nil {
-		return nil, err
-	}
-	var conflicts []Conflict
-	for _, dependent := range dependents {
-		if dependent.Dependency.Version == "" {
-			continue
-		} else if c, err := dm.CheckConflict(version, dependent.Dependency); err != nil {
+		if err := g.Add(pkg.Name, installedVersion, deps, len(pkg.OwnerReferences) == 0); err != nil {
 			return nil, err
-		} else if c != nil {
-			conflicts = append(conflicts, *c)
 		}
 	}
-	return conflicts, nil
+	return g, nil
 }
 
-type GetDependentsOptions struct {
-	PackageList         *v1alpha1.PackageList
-	IncludeDependencies bool
+func (dm *DependendcyManager) add(
+	g *graph.DependencyGraph,
+	manifest v1alpha1.PackageManifest,
+	version string,
+) error {
+	return g.Add(manifest.Name, version, manifest.Dependencies, g.Manual(manifest.Name))
 }
 
-func (dm *DependendcyManager) GetDependents(ctx context.Context, pkg *v1alpha1.Package, options GetDependentsOptions) ([]dependentTuple, error) {
-	var owningPkgs []v1alpha1.Package
-
-	if ownersOfType, err := dm.OwnersOfType(&v1alpha1.Package{}, pkg); err != nil {
-		return nil, err
-	} else if len(ownersOfType) > 0 {
-		// if there is at least one Package owning it, means that all ownersOfType are represented as owner references
-		for _, owner := range ownersOfType {
-			if owningPkg, err := dm.clientAdapter.GetPackage(ctx, owner.Name); err != nil || owningPkg == nil {
+// addDependencies adds the highest possible version of every uninstalled dependency and installs all transitive
+// dependencies
+func (dm *DependendcyManager) addDependencies(
+	g *graph.DependencyGraph,
+	name string,
+	transitive bool,
+) ([]Requirement, error) {
+	var allAdded []Requirement
+	for _, dep := range g.Dependencies(name) {
+		if g.Version(dep) == nil {
+			if versions, err := dm.getVersions(dep); err != nil {
+				return nil, err
+			} else if maxVersion, err := g.Max(dep, versions); err != nil {
+				// This error occurs when no suitable version exists.
+				// In this case, the dependency is not added to the graph and a validation error detects this later.
+				continue
+			} else if depManifest, err := dm.repoAdapter.GetManifest("", dep, maxVersion.Original()); err != nil {
+				return nil, err
+			} else if err := dm.add(g, *depManifest, maxVersion.Original()); err != nil {
+				return nil, err
+			} else if added, err := dm.addDependencies(g, depManifest.Name, true); err != nil {
 				return nil, err
 			} else {
-				owningPkgs = append(owningPkgs, *owningPkg)
-			}
-		}
-	} else {
-		// otherwise we get the owners by iterating over all packages and search for matching OwnedPackages
-		pkgRef, err := utils.ToOwnedResourceRef(dm.GetScheme(), pkg)
-		if err != nil {
-			return nil, err
-		}
-
-		var pkgs *v1alpha1.PackageList
-		if options.PackageList != nil {
-			pkgs = options.PackageList
-		} else if pkgs, err = dm.clientAdapter.ListPackages(ctx); err != nil {
-			return nil, err
-		}
-
-		for _, otherPkg := range pkgs.Items {
-			for _, ownedPkgRef := range otherPkg.Status.OwnedPackages {
-				if !ownedPkgRef.MarkedForDeletion && ownedPkgRef == pkgRef {
-					owningPkgs = append(owningPkgs, otherPkg)
-				}
+				allAdded = append(allAdded, Requirement{
+					PackageWithVersion: PackageWithVersion{Name: depManifest.Name, Version: maxVersion.Original()},
+					Transitive:         transitive})
+				allAdded = append(allAdded, added...)
 			}
 		}
 	}
+	return allAdded, nil
+}
 
-	var dependents []dependentTuple
-	for i, owningPkg := range owningPkgs {
-		if options.IncludeDependencies {
-			if dt, err := dm.getDependency(ctx, &owningPkg, pkg.Name); err != nil {
-				return nil, err
-			} else if dt != nil {
-				dependents = append(dependents, *dt)
-			}
-		} else {
-			dependents = append(dependents, dependentTuple{Pkg: &owningPkgs[i]})
+// errorToConflict returns a Conflict if the error is a graph.ConstraintError. Otherwise, it returns the error
+// unmodified
+func errorToConflict(err error) (*Conflict, error) {
+	if errConstraint := (&graph.ConstraintError{}); errors.As(err, &errConstraint) {
+		var version string
+		var constraint string
+		if errConstraint.Version != nil {
+			version = errConstraint.Version.Original()
 		}
+		if errConstraint.Constraint != nil {
+			constraint = errConstraint.Constraint.String()
+		}
+		return &Conflict{
+			Actual:   PackageWithVersion{Name: errConstraint.Name, Version: version},
+			Required: PackageWithVersion{Name: errConstraint.Name, Version: constraint},
+			Cause:    err,
+		}, nil
+	} else {
+		return nil, err
 	}
-
-	return dependents, nil
 }
 
-func (dm *DependendcyManager) getDependency(ctx context.Context, pkg *v1alpha1.Package, dependency string) (*dependentTuple, error) {
-	pkgInfo, err := dm.clientAdapter.GetPackageInfo(ctx, pkg.Status.OwnedPackageInfos[0].Name)
-	if err != nil {
+// getVersions is a utility to get all versions for a package from repoAdapter and also parse them
+func (dm *DependendcyManager) getVersions(name string) ([]*semver.Version, error) {
+	if versions, err := dm.repoAdapter.GetVersions("", name); err != nil {
 		return nil, err
-	}
-	if pkgInfo.Status.Manifest == nil {
-		return nil, nil
-	}
-	for _, dep := range pkgInfo.Status.Manifest.Dependencies {
-		if dep.Name == dependency {
-			return &dependentTuple{
-				Pkg:        pkg,
-				Dependency: dep,
-			}, nil
+	} else {
+		parsedVersions := make([]*semver.Version, len(versions))
+		for i, version := range versions {
+			parsedVersions[i], err = semver.NewVersion(version)
+			if err != nil {
+				return nil, err
+			}
 		}
+		return parsedVersions, nil
 	}
-	return nil, nil
 }
diff --git a/internal/dependency/types.go b/internal/dependency/types.go
new file mode 100644
index 0000000..0364a85
--- /dev/null
+++ b/internal/dependency/types.go
@@ -0,0 +1,50 @@
+package dependency
+
+import (
+	"fmt"
+	"strings"
+)
+
+type ValidationResultStatus string
+
+const (
+	ValidationResultStatusOk         ValidationResultStatus = "OK"
+	ValidationResultStatusResolvable ValidationResultStatus = "RESOLVABLE"
+	ValidationResultStatusConflict   ValidationResultStatus = "CONFLICT"
+)
+
+type PackageWithVersion struct {
+	Name    string
+	Version string
+}
+
+type Requirement struct {
+	PackageWithVersion
+	Transitive bool
+}
+
+type Conflict struct {
+	Actual   PackageWithVersion
+	Required PackageWithVersion
+	Cause    error
+}
+
+func (cf Conflict) String() string {
+	return fmt.Sprintf("%v (required: %v, actual: %v)", cf.Required.Name, cf.Required.Version, cf.Actual.Version)
+}
+
+type Conflicts []Conflict
+
+func (cf Conflicts) String() string {
+	s := make([]string, len(cf))
+	for i, c := range cf {
+		s[i] = c.String()
+	}
+	return strings.Join(s, ", ")
+}
+
+type ValidationResult struct {
+	Status       ValidationResultStatus
+	Requirements []Requirement
+	Conflicts    Conflicts
+}
diff --git a/internal/names/package_info.go b/internal/names/package_info.go
new file mode 100644
index 0000000..c8a7953
--- /dev/null
+++ b/internal/names/package_info.go
@@ -0,0 +1,11 @@
+package names
+
+import (
+	"fmt"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+)
+
+func PackageInfoName(pkg v1alpha1.Package) string {
+	return escapeResourceName(fmt.Sprintf("%v--%v", pkg.Spec.PackageInfo.Name, pkg.Spec.PackageInfo.Version))
+}
diff --git a/internal/controller/utils.go b/internal/names/utils.go
similarity index 91%
rename from internal/controller/utils.go
rename to internal/names/utils.go
index f0fe0ec..3a5a1a7 100644
--- a/internal/controller/utils.go
+++ b/internal/names/utils.go
@@ -1,4 +1,4 @@
-package controller
+package names
 
 import (
 	"regexp"
diff --git a/internal/repo/client/fake/fakeclient.go b/internal/repo/client/fake/fakeclient.go
index 3a0bee0..e1f42d5 100644
--- a/internal/repo/client/fake/fakeclient.go
+++ b/internal/repo/client/fake/fakeclient.go
@@ -10,21 +10,21 @@ import (
 
 // FakeClient is a mock implementation of RepoClient for use in tests
 type FakeClient struct {
-	Packages map[string]map[string]v1alpha1.PackageManifest
+	Packages map[string]map[string]*v1alpha1.PackageManifest
 }
 
-func (f *FakeClient) AddPackage(name, version string, manifest v1alpha1.PackageManifest) {
+func (f *FakeClient) AddPackage(name, version string, manifest *v1alpha1.PackageManifest) {
 	if f.Packages == nil {
 		f.Clear()
 	}
 	if _, ok := f.Packages[name]; !ok {
-		f.Packages[name] = map[string]v1alpha1.PackageManifest{}
+		f.Packages[name] = map[string]*v1alpha1.PackageManifest{}
 	}
 	f.Packages[name][version] = manifest
 }
 
 func (f *FakeClient) Clear() {
-	f.Packages = map[string]map[string]v1alpha1.PackageManifest{}
+	f.Packages = map[string]map[string]*v1alpha1.PackageManifest{}
 }
 
 var _ client.RepoClient = &FakeClient{}
@@ -33,7 +33,7 @@ var _ client.RepoClient = &FakeClient{}
 func (f *FakeClient) FetchLatestPackageManifest(repoURL string, name string, target *v1alpha1.PackageManifest) (version string, err error) {
 	if versions, ok := f.Packages[name]; ok {
 		for v, m := range versions {
-			*target = m
+			*target = *m
 			return v, nil
 		}
 	}
@@ -58,7 +58,7 @@ func (f *FakeClient) FetchPackageIndex(repoURL string, name string, target *type
 func (f *FakeClient) FetchPackageManifest(repoURL string, name string, version string, target *v1alpha1.PackageManifest) error {
 	if versions, ok := f.Packages[name]; ok {
 		if manifest, ok := versions[version]; ok {
-			*target = manifest
+			*target = *manifest
 			return nil
 		}
 	}
diff --git a/internal/semver/compare.go b/internal/semver/compare.go
index 70c5ae9..d6ff27e 100644
--- a/internal/semver/compare.go
+++ b/internal/semver/compare.go
@@ -11,45 +11,38 @@ import (
 // Am important deviation from the semver standard is that this function DOES try to interpret
 // the version metadata as a number when comparing.
 func IsUpgradable(installed, desired string) bool {
-	var parsedInstalled, parsedDesired *semver.Version
-	var err error
-	if parsedInstalled, err = semver.NewVersion(installed); err != nil {
-		parsedInstalled = nil
-	} else if parsedDesired, err = semver.NewVersion(desired); err != nil {
-		parsedDesired = nil
-	}
-	if parsedDesired != nil && parsedInstalled != nil {
-		if parsedDesired.GreaterThan(parsedInstalled) {
-			return true
-		} else if parsedDesired.Equal(parsedInstalled) {
-			return isUpgradableMetadata(parsedInstalled, parsedDesired)
-		} else {
-			return false
-		}
-	} else {
+	if parsedInstalled, err := semver.NewVersion(installed); err != nil {
+		return installed != desired
+	} else if parsedDesired, err := semver.NewVersion(desired); err != nil {
 		return installed != desired
+	} else {
+		return IsVersionUpgradable(parsedInstalled, parsedDesired)
 	}
 }
 
-func isUpgradableMetadata(installed, latest *semver.Version) bool {
-	latestMeta := latest.Metadata()
+func IsVersionUpgradable(installed, desired *semver.Version) bool {
+	return desired.GreaterThan(installed) || (desired.Equal(installed) && isUpgradableMetadata(installed, desired))
+}
+
+func isUpgradableMetadata(installed, desired *semver.Version) bool {
+	desiredMeta := desired.Metadata()
 	installedMeta := installed.Metadata()
 
-	if latestMeta == installedMeta {
+	if desiredMeta == installedMeta {
 		return false
 	}
 
-	if latestMeta == "" {
+	if desiredMeta == "" {
 		return false
 	} else if installedMeta == "" {
 		return true
 	}
 
-	if latestMetaInt, err := strconv.Atoi(latestMeta); err != nil {
-		return installedMeta != latestMeta
+	if desiredMetadataInt, err := strconv.Atoi(desiredMeta); err != nil {
+		return installedMeta != desiredMeta
 	} else if installedMetaInt, err := strconv.Atoi(installedMeta); err != nil {
-		return installedMeta != latestMeta
+		return installedMeta != desiredMeta
 	} else {
-		return installedMetaInt < latestMetaInt
+		return installedMetaInt < desiredMetadataInt
 	}
 }
diff --git a/internal/semver/errors.go b/internal/semver/errors.go
new file mode 100644
index 0000000..b29b2b9
--- /dev/null
+++ b/internal/semver/errors.go
@@ -0,0 +1,26 @@
+package semver
+
+import "go.uber.org/multierr"
+
+type ConstraintValidationError []error
+
+func ErrConstraintValidation(errors []error) error {
+	if len(errors) > 0 {
+		return (*ConstraintValidationError)(&errors)
+	} else {
+		return nil
+	}
+}
+
+func (err *ConstraintValidationError) Error() string {
+	return multierr.Combine(*err...).Error()
+}
+
+func (err *ConstraintValidationError) Unwrap() []error {
+	return *err
+}
+
+func (err *ConstraintValidationError) Is(other error) bool {
+	_, ok := other.(*ConstraintValidationError)
+	return ok
+}
diff --git a/internal/semver/validate.go b/internal/semver/validate.go
new file mode 100644
index 0000000..22350ef
--- /dev/null
+++ b/internal/semver/validate.go
@@ -0,0 +1,18 @@
+package semver
+
+import "github.com/Masterminds/semver/v3"
+
+func ValidateConstraint(version, constraint string) error {
+	if parsedVersion, err := semver.NewVersion(version); err != nil {
+		return err
+	} else if parsedConstraint, err := semver.NewConstraint(constraint); err != nil {
+		return err
+	} else {
+		return ValidateVersionConstraint(parsedVersion, parsedConstraint)
+	}
+}
+
+func ValidateVersionConstraint(version *semver.Version, constraint *semver.Constraints) error {
+	_, err := constraint.Validate(version)
+	return ErrConstraintValidation(err)
+}
diff --git a/internal/web/server.go b/internal/web/server.go
index ad2be11..c4cd27c 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -56,7 +56,6 @@ var webFs fs.FS = embeddedFs
 func init() {
 	if config.IsDevBuild() {
 		if _, err := os.Lstat(templatesBaseDir); err == nil {
-			fmt.Println("using DirFS")
 			webFs = os.DirFS(templatesBaseDir)
 		}
 	}
@@ -143,17 +142,16 @@ func (s *server) Start(ctx context.Context) error {
 		return errors.New("server is already listening")
 	}
 
-	_ = s.initKubeConfig()
-	if err := s.checkBootstrapped(ctx); err == nil {
-		s.startInformer(ctx)
-	}
-
+	parseTemplates()
 	if config.IsDevBuild() {
 		if err := watchTemplates(); err != nil {
 			fmt.Fprintf(os.Stderr, "templates will not be parsed after changes: %v\n", err)
 		}
 	}
-	parseTemplates()
+	_ = s.initKubeConfig()
+	if err := s.checkBootstrapped(ctx); err == nil {
+		s.startInformer(ctx)
+	}
 
 	root, err := fs.Sub(webFs, "root")
 	if err != nil {
@@ -257,7 +255,7 @@ func (s *server) installModal(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	res, err := s.dependencyMgr.Validate(r.Context(), &mf)
+	res, err := s.dependencyMgr.Validate(r.Context(), &mf, selectedVersion)
 	if err != nil {
 		s.respondAlertAndLog(w, err, fmt.Sprintf("An error occurred validating dependencies of %v in version %v", pkgName, selectedVersion))
 		return
diff --git a/internal/webhook/errors.go b/internal/webhook/errors.go
index cb2c8d7..ad591e1 100644
--- a/internal/webhook/errors.go
+++ b/internal/webhook/errors.go
@@ -4,26 +4,18 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/dependency"
 )
 
 var ErrInvalidObject = errors.New("validator called with unexpected object type")
 var ErrDependencyConflict = errors.New("dependency conflict")
-var ErrTransitiveDependency = errors.New("support for transitive dependencies is not implemented yet")
 
 func newConflictError(conflicts dependency.Conflicts) error {
 	return fmt.Errorf("%w: %v", ErrDependencyConflict, conflicts)
 }
 
-func newConflictErrorDelete(owner string) error {
-	return fmt.Errorf("%w: dependency of %v", ErrDependencyConflict, owner)
+func newConflictErrorDelete(err error) error {
+	return fmt.Errorf("%w: %w", ErrDependencyConflict, err)
 }
 
-func newTransitiveError(requirement dependency.PackageWithVersion, dependency v1alpha1.Dependency) error {
-	return fmt.Errorf("%w: requirement %v (%v) depends on %v (%v)",
-		ErrTransitiveDependency, requirement.Name, requirement.Version, dependency.Name, dependency.Version)
-}
-
-func isErrDependencyConflict(err error) bool   { return errors.Is(err, ErrDependencyConflict) }
-func isErrTransitiveDependency(err error) bool { return errors.Is(err, ErrTransitiveDependency) }
+func isErrDependencyConflict(err error) bool { return errors.Is(err, ErrDependencyConflict) }
diff --git a/internal/webhook/package_validator.go b/internal/webhook/package_validator.go
index d5d853b..25e2273 100644
--- a/internal/webhook/package_validator.go
+++ b/internal/webhook/package_validator.go
@@ -2,11 +2,13 @@ package webhook
 
 import (
 	"context"
+	"errors"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/controller/owners"
 	"github.com/glasskube/glasskube/internal/dependency"
 	ctrladapter "github.com/glasskube/glasskube/internal/dependency/adapter/controllerruntime"
+	"github.com/glasskube/glasskube/internal/dependency/graph"
 	"github.com/glasskube/glasskube/internal/repo"
 	repoclient "github.com/glasskube/glasskube/internal/repo/client"
 	"go.uber.org/multierr"
@@ -77,13 +79,16 @@ func (p *PackageValidatingWebhook) ValidateDelete(ctx context.Context, obj runti
 	log := ctrl.LoggerFrom(ctx)
 	if pkg, ok := obj.(*v1alpha1.Package); ok {
 		log.Info("validate delete", "name", pkg.Name)
-		if dependants, err := p.GetDependents(ctx, pkg, dependency.GetDependentsOptions{IncludeDependencies: false}); err != nil {
+		if g, err := p.NewGraph(ctx); err != nil {
 			return nil, err
-		} else if len(dependants) > 0 {
-			for _, dep := range dependants {
-				if dep.Pkg.DeletionTimestamp.IsZero() {
-					return nil, newConflictErrorDelete(dep.Pkg.Name)
+		} else {
+			if _, err := g.ValidateDelete(pkg.Name); err != nil {
+				for _, err1 := range multierr.Errors(err) {
+					if !errors.Is(err1, &graph.DependencyError{}) {
+						return nil, err1
+					}
 				}
+				return nil, newConflictErrorDelete(err)
 			}
 		}
 		return nil, nil
@@ -99,28 +104,11 @@ func (p *PackageValidatingWebhook) validateCreateOrUpdate(ctx context.Context, p
 		return err
 	}
 
-	if result, err := p.Validate(ctx, &manifest); err != nil {
+	if result, err := p.Validate(ctx, &manifest, pkg.Spec.PackageInfo.Version); err != nil {
 		return err
-	} else if parentConflicts, err := p.IsUpdateAllowed(ctx, pkg, pkg.Spec.PackageInfo.Version); err != nil {
-		return err
-	} else if len(result.Conflicts) > 0 || len(parentConflicts) > 0 {
+	} else if len(result.Conflicts) > 0 {
 		// Conflicts are not allowed.
-		return newConflictError(append(result.Conflicts, parentConflicts...))
-	} else if len(result.Requirements) > 0 {
-		// Transitive dependencies are not supported yet, so we validate that a required package does not have any
-		// dependencies itself.
-		// TODO: Add support for validating transitive dependencies.
-		var errs error
-		for _, req := range result.Requirements {
-			var depManifest v1alpha1.PackageManifest
-			err := p.repo.FetchPackageManifest("", req.Name, req.Version, &depManifest)
-			if err != nil {
-				errs = multierr.Append(errs, err)
-			} else if len(depManifest.Dependencies) > 0 {
-				errs = multierr.Append(errs, newTransitiveError(req, depManifest.Dependencies[0]))
-			}
-		}
-		return errs
+		return newConflictError(result.Conflicts)
 	} else {
 		return nil
 	}
diff --git a/pkg/list/list.go b/pkg/list/list.go
index 5d635b1..21f5d77 100644
--- a/pkg/list/list.go
+++ b/pkg/list/list.go
@@ -6,6 +6,7 @@ import (
 	"sync"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/names"
 	"github.com/glasskube/glasskube/internal/repo"
 	"github.com/glasskube/glasskube/pkg/client"
 	"go.uber.org/multierr"
@@ -120,14 +121,11 @@ func fetchRepoAndInstalled(pkgClient client.PackageV1Alpha1Client, ctx context.C
 		for j, clusterPackage := range packages.Items {
 			if indexPackage.Name == clusterPackage.Name {
 				result[i].Package = &packages.Items[j]
-				if len(clusterPackage.Status.OwnedPackageInfos) > 0 {
-					// TODO: It would be better to use .Spec.PackageInfo to get the name of the correct PackageInfo
-					packageInfoName := clusterPackage.Status.OwnedPackageInfos[len(clusterPackage.Status.OwnedPackageInfos)-1].Name
-					for k, packageInfo := range packageInfos.Items {
-						if packageInfo.Name == packageInfoName {
-							result[i].PackageInfo = &packageInfos.Items[k]
-							break
-						}
+				packageInfoName := names.PackageInfoName(clusterPackage)
+				for k, packageInfo := range packageInfos.Items {
+					if packageInfo.Name == packageInfoName {
+						result[i].PackageInfo = &packageInfos.Items[k]
+						break
 					}
 				}
 				break
diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go
index 5bdaa61..03eb481 100644
--- a/pkg/manifest/manifest.go
+++ b/pkg/manifest/manifest.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 
+	"github.com/glasskube/glasskube/internal/names"
 	"github.com/glasskube/glasskube/pkg/client"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
@@ -23,12 +24,8 @@ func GetInstalledManifest(ctx context.Context, pkgName string) (*v1alpha1.Packag
 
 func GetInstalledManifestForPackage(ctx context.Context, pkg v1alpha1.Package) (*v1alpha1.PackageManifest, error) {
 	pkgClient := client.FromContext(ctx)
-	if len(pkg.Status.OwnedPackageInfos) == 0 {
-		return nil, ErrPackageNoOwnedPackageInfo
-	}
-	packageInfoName := pkg.Status.OwnedPackageInfos[len(pkg.Status.OwnedPackageInfos)-1].Name
 	var packageInfo v1alpha1.PackageInfo
-	if err := pkgClient.PackageInfos().Get(ctx, packageInfoName, &packageInfo); err != nil {
+	if err := pkgClient.PackageInfos().Get(ctx, names.PackageInfoName(pkg), &packageInfo); err != nil {
 		return nil, err
 	} else if packageInfo.Status.Manifest != nil {
 		return packageInfo.Status.Manifest, nil
diff --git a/pkg/update/update.go b/pkg/update/update.go
index bf39fd3..2cff81a 100644
--- a/pkg/update/update.go
+++ b/pkg/update/update.go
@@ -22,7 +22,7 @@ import (
 type UpdateTransaction struct {
 	Items         []updateTransactionItem
 	ConflictItems []updateTransactionItemConflict
-	Requirements  []dependency.PackageWithVersion
+	Requirements  []dependency.Requirement
 }
 
 func (tx UpdateTransaction) IsEmpty() bool {
@@ -99,7 +99,7 @@ func (c *updater) Prepare(ctx context.Context, packageNames []string) (*UpdateTr
 		return nil, fmt.Errorf("failed to fetch index: %v", err)
 	}
 
-	requirementsSet := make(map[dependency.PackageWithVersion]struct{})
+	requirementsSet := make(map[dependency.Requirement]struct{})
 	var tx UpdateTransaction
 outer:
 	for _, pkg := range packagesToUpdate {
@@ -111,13 +111,11 @@ outer:
 					if err := repo.FetchPackageManifest("", pkg.Name, indexItem.LatestVersion, &manifest); err != nil {
 						return nil, err
 					}
-					if result, err := c.dm.Validate(ctx, &manifest); err != nil {
+					if result, err := c.dm.Validate(ctx, &manifest, indexItem.LatestVersion); err != nil {
 						return nil, err
-					} else if cf, err := c.dm.IsUpdateAllowed(ctx, &pkg, indexItem.LatestVersion); err != nil {
-						return nil, err
-					} else if len(result.Conflicts) > 0 || len(cf) > 0 {
+					} else if len(result.Conflicts) > 0 {
 						// This package can't be updated due to conflicts
-						tx.ConflictItems = append(tx.ConflictItems, updateTransactionItemConflict{item, append(result.Conflicts, cf...)})
+						tx.ConflictItems = append(tx.ConflictItems, updateTransactionItemConflict{item, result.Conflicts})
 					} else {
 						for _, req := range result.Requirements {
 							requirementsSet[req] = struct{}{}
diff --git a/website/blog/authors.yml b/website/blog/authors.yml
index a3d40e3..3636cfd 100644
--- a/website/blog/authors.yml
+++ b/website/blog/authors.yml
@@ -2,22 +2,22 @@ pmig:
   name: Philip Miglinci
   title: Co-Founder
   url: https://www.linkedin.com/in/pmigat/
-  image_url: https://cms.glasskube.eu/uploads/1_81920acfa3.png
+  image_url: https://avatars.githubusercontent.com/pmig
 
 lw:
   name: Louis Weston
   title: Co-Founder
-  url: https://www.linkedin.com/in/louiswaefler/
-  image_url: https://cms.glasskube.eu/uploads/2_5973424c16.png
+  url: https://www.linkedin.com/in/louisnweston/
+  image_url: https://avatars.githubusercontent.com/thekubernaut
 
 christophenne:
   name: Christoph Enne
   title: Software Engineer
   url: https://github.com/christophenne/
-  image_url: https://avatars.githubusercontent.com/u/7556574
+  image_url: https://avatars.githubusercontent.com/christophenne
 
 jpage:
   name: Jake Page
   title: Developer Relations Engineer
   url: https://github.com/jakepage91
-  image_url: https://avatars.githubusercontent.com/jakepage91
\ No newline at end of file
+  image_url: https://avatars.githubusercontent.com/jakepage91
diff --git a/website/docs/03_components/03_package-repository.md b/website/docs/03_components/03_package-repository.md
index 3e8a131..0edd89f 100644
--- a/website/docs/03_components/03_package-repository.md
+++ b/website/docs/03_components/03_package-repository.md
@@ -51,3 +51,6 @@ Sometimes it is important to pin down what versions of a dependency should be us
 Often, this will not only be one version, but a range of versions. There is no common specification for semver ranges, 
 but there seem to be some common expectations of to what version range specifications look like.
 We mostly rely on the [Masterminds/semver](https://github.com/Masterminds/semver) package to do version constraint checks, which itself works with version range syntax close to js/npm and Rust/Cargo. 
+
+Please note that we do not allow build numbers to be part of such version ranges. 
+If there is some change in a required package, that would make a dependant package incompatible, this change needs to be reflected in the actual app version anyway. 



Test patch:
diff --git a/internal/dependency/graph/graph_suite_test.go b/internal/dependency/graph/graph_suite_test.go
new file mode 100644
index 0000000..6b6b1b4
--- /dev/null
+++ b/internal/dependency/graph/graph_suite_test.go
@@ -0,0 +1,13 @@
+package graph
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestDependencyGraph(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Graph Suite")
+}
diff --git a/internal/dependency/graph/graph_test.go b/internal/dependency/graph/graph_test.go
new file mode 100644
index 0000000..5a134cd
--- /dev/null
+++ b/internal/dependency/graph/graph_test.go
@@ -0,0 +1,177 @@
+package graph
+
+import (
+	"github.com/Masterminds/semver/v3"
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("DependencyGraph", func() {
+	var graph *DependencyGraph
+	var foo = "foo"
+	var bar = "bar"
+	var baz = "baz"
+
+	BeforeEach(func() {
+		graph = NewGraph()
+	})
+
+	Describe("Validate", func() {
+		When("graph is empty", func() {
+			It("should not return an error", func() {
+				Expect(graph.Validate()).NotTo(HaveOccurred())
+			})
+		})
+
+		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.Validate()).NotTo(HaveOccurred())
+			})
+		})
+
+		When("there is a package with dependencies", 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.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.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.Validate()).To(MatchError(&DependencyError{}))
+					})
+				})
+			})
+
+			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.Validate()).To(MatchError(&DependencyError{}))
+				})
+			})
+		})
+	})
+
+	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.Version(foo)).NotTo(BeNil())
+			Expect(graph.Manual(foo)).To(BeTrue())
+			Expect(graph.Dependencies(foo)).NotTo(BeEmpty())
+			Expect(graph.Delete(foo)).To(BeTrue())
+			Expect(graph.Version(foo)).To(BeNil())
+			Expect(graph.Manual(foo)).To(BeFalse())
+			Expect(graph.Dependencies(foo)).To(BeEmpty())
+		})
+	})
+
+	Describe("Prune", func() {
+		It("should remove orphaned vertex", func() {
+			Expect(graph.Add(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())
+		})
+	})
+
+	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.Version(bar)).To(Equal(semver.MustParse("v1.0.0")))
+			Expect(graph.DeleteAndPrune(foo)).To(ConsistOf(foo, bar))
+			Expect(graph.Version(bar)).To(BeNil())
+		})
+	})
+
+	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)).
+				NotTo(HaveOccurred())
+			Expect(graph.Add(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)).
+				NotTo(HaveOccurred())
+			Expect(graph.Add(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)
+			Expect(err).To(HaveOccurred())
+			Expect(v).To(BeNil())
+		})
+
+		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)).
+				NotTo(HaveOccurred())
+			Expect(graph.Add(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")}
+			v, err := graph.Max(bar, versions)
+			Expect(err).NotTo(HaveOccurred())
+			Expect(v).To(Equal(semver.MustParse("1.1.0")))
+		})
+	})
+
+	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)).
+				NotTo(HaveOccurred())
+			Expect(graph.Dependencies(foo)).To(ConsistOf(bar, baz))
+		})
+	})
+
+	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))
+		})
+	})
+
+	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")))
+		})
+	})
+
+	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())
+			newGraph := graph.DeepCopy()
+			Expect(graph.Add(bar, "v1.1.0", nil, false)).NotTo(HaveOccurred())
+			Expect(newGraph).NotTo(Equal(graph))
+		})
+	})
+})
+
+func constraint(pattern string) *semver.Constraints {
+	c, err := semver.NewConstraint(pattern)
+	Expect(err).NotTo(HaveOccurred())
+	return c
+}
diff --git a/internal/dependency/manager_test.go b/internal/dependency/manager_test.go
index 4426dd8..7021ba6 100644
--- a/internal/dependency/manager_test.go
+++ b/internal/dependency/manager_test.go
@@ -5,32 +5,23 @@ import (
 	"strings"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/repo/client/fake"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-type testClientAdapter struct {
-}
+var fakeRepo = &fake.FakeClient{}
 
-func (a *testClientAdapter) GetPackage(ctx context.Context, pkgName string) (*v1alpha1.Package, error) {
-	if pkgName == "P" && p != nil {
-		return p, nil
-	} else if pkgName == "D" && d != nil {
-		return d, nil
-	} else if pkgName == "E" && e != nil {
-		return e, nil
-	} else {
-		return nil, nil
-	}
+type testClientAdapter struct {
 }
 
 func (a *testClientAdapter) GetPackageInfo(ctx context.Context, pkgInfoName string) (*v1alpha1.PackageInfo, error) {
-	if strings.HasPrefix(pkgInfoName, "P") && pi != nil {
+	if strings.HasPrefix(pkgInfoName, "p") && pi != nil {
 		return pi, nil
-	} else if strings.HasPrefix(pkgInfoName, "D") && di != nil {
+	} else if strings.HasPrefix(pkgInfoName, "d") && di != nil {
 		return di, nil
-	} else if strings.HasPrefix(pkgInfoName, "E") && ei != nil {
+	} else if strings.HasPrefix(pkgInfoName, "e") && ei != nil {
 		return ei, nil
 	} else {
 		return nil, nil
@@ -53,27 +44,14 @@ func (a *testClientAdapter) ListPackages(ctx context.Context) (*v1alpha1.Package
 	}, nil
 }
 
-type testRepoAdapter struct {
-}
-
-func (a *testRepoAdapter) GetLatestVersion(repo string, pkgName string) (string, error) {
-	return latestVersion, nil
-}
-
-func (a *testRepoAdapter) GetMaxVersionCompatibleWith(repo string, pkgName string, versionRange string) (string, error) {
-	return latestVersion, nil
-}
-
 func createDependencyManager() *DependendcyManager {
-	return &DependendcyManager{
-		clientAdapter: &testClientAdapter{},
-		repoAdapter:   &testRepoAdapter{},
-	}
+	return NewDependencyManager(&testClientAdapter{}, nil).WithRepo(fakeRepo)
 }
 
 var dm *DependendcyManager
 
-// For the following test suite, we always use the Package p (name "P") as the package who's dependencies should be checked
+// For the following test suite, we always use the Package p (name "P") as the package who's dependencies should be
+// checked.
 // 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").
@@ -82,10 +60,11 @@ var d, e, p, x, y *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
 
-// latestVersion is used as a mock string which will be returned by the testRepoAdapter
-var latestVersion string
-
 func createPackageAndInfo(name string, version string) (*v1alpha1.Package, *v1alpha1.PackageInfo) {
+	manifest := v1alpha1.PackageManifest{
+		Name: name,
+	}
+	fakeRepo.AddPackage(name, version, &manifest)
 	return &v1alpha1.Package{
 			ObjectMeta: v1.ObjectMeta{
 				Name: name,
@@ -96,16 +75,15 @@ func createPackageAndInfo(name string, version string) (*v1alpha1.Package, *v1al
 					Version: version,
 				},
 			},
+			Status: v1alpha1.PackageStatus{OwnedPackageInfos: []v1alpha1.OwnedResourceRef{{Name: name}}},
 		}, &v1alpha1.PackageInfo{
 			Spec: v1alpha1.PackageInfoSpec{
 				Name:    name,
 				Version: version,
 			},
 			Status: v1alpha1.PackageInfoStatus{
-				Version: version,
-				Manifest: &v1alpha1.PackageManifest{
-					Name: name,
-				},
+				Version:  version,
+				Manifest: &manifest,
 			},
 		}
 }
@@ -118,6 +96,7 @@ var _ = Describe("Dependency Manager", func() {
 	})
 
 	AfterEach(func() {
+		fakeRepo.Clear()
 		p = nil
 		pi = nil
 		d = nil
@@ -129,14 +108,13 @@ var _ = Describe("Dependency Manager", func() {
 		y = nil
 		yi = nil
 		dm = nil
-		latestVersion = ""
 	})
 
 	Describe("Validation", func() {
 
 		When("P has no dependencies", func() {
 			It("should return OK", func(ctx context.Context) {
-				res, err := dm.Validate(ctx, pi.Status.Manifest)
+				res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 				Expect(err).ShouldNot(HaveOccurred())
 				Expect(res).ShouldNot(BeNil())
 				Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -163,7 +141,7 @@ var _ = Describe("Dependency Manager", func() {
 
 					When("no other package dependent on D", func() {
 						It("should return OK", func(ctx context.Context) {
-							res, err := dm.Validate(ctx, pi.Status.Manifest)
+							res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 							Expect(err).ShouldNot(HaveOccurred())
 							Expect(res).ShouldNot(BeNil())
 							Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -187,7 +165,7 @@ var _ = Describe("Dependency Manager", func() {
 
 						When("X and Y require no version range of D", func() {
 							It("should return OK", func(ctx context.Context) {
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -199,7 +177,7 @@ var _ = Describe("Dependency Manager", func() {
 						When("X requires D in version range", func() {
 							It("should return OK", func(ctx context.Context) {
 								xi.Status.Manifest.Dependencies[0].Version = ">= 1, < 2"
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -211,7 +189,7 @@ var _ = Describe("Dependency Manager", func() {
 						When("Y requires D in version range", func() {
 							It("should return OK", func(ctx context.Context) {
 								yi.Status.Manifest.Dependencies[0].Version = "1.x.x"
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -224,7 +202,7 @@ var _ = Describe("Dependency Manager", func() {
 							It("should return OK", func(ctx context.Context) {
 								xi.Status.Manifest.Dependencies[0].Version = ">= 1, < 2"
 								yi.Status.Manifest.Dependencies[0].Version = "1.x.x"
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -236,10 +214,11 @@ var _ = Describe("Dependency Manager", func() {
 				})
 
 				When("D does not exist", func() {
-
+					BeforeEach(func() {
+						fakeRepo.AddPackage("D", "1.1.7", &v1alpha1.PackageManifest{Name: "D"})
+					})
 					It("should return a RESOLVABLE result with D in latest", func(ctx context.Context) {
-						latestVersion = "1.1.7"
-						res, err := dm.Validate(ctx, pi.Status.Manifest)
+						res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 						Expect(err).ShouldNot(HaveOccurred())
 						Expect(res).ShouldNot(BeNil())
 						Expect(res.Status).Should(Equal(ValidationResultStatusResolvable))
@@ -271,7 +250,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.3.0")
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -281,7 +260,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")
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusConflict))
@@ -296,7 +275,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")
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusConflict))
@@ -327,7 +306,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.3.1")
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -337,7 +316,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")
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusConflict))
@@ -352,7 +331,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")
-								res, err := dm.Validate(ctx, pi.Status.Manifest)
+								res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 								Expect(err).ShouldNot(HaveOccurred())
 								Expect(res).ShouldNot(BeNil())
 								Expect(res.Status).Should(Equal(ValidationResultStatusConflict))
@@ -384,7 +363,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")
-							res, err := dm.Validate(ctx, pi.Status.Manifest)
+							res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 							Expect(err).ShouldNot(HaveOccurred())
 							Expect(res).ShouldNot(BeNil())
 							Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -394,7 +373,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")
-							res, err := dm.Validate(ctx, pi.Status.Manifest)
+							res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 							Expect(err).ShouldNot(HaveOccurred())
 							Expect(res).ShouldNot(BeNil())
 							Expect(res.Status).Should(Equal(ValidationResultStatusConflict))
@@ -409,7 +388,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")
-							res, err := dm.Validate(ctx, pi.Status.Manifest)
+							res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 							Expect(err).ShouldNot(HaveOccurred())
 							Expect(res).ShouldNot(BeNil())
 							Expect(res.Status).Should(Equal(ValidationResultStatusConflict))
@@ -425,10 +404,12 @@ var _ = Describe("Dependency Manager", func() {
 				})
 
 				When("D does not exist", func() {
+					BeforeEach(func() {
+						fakeRepo.AddPackage("D", "1.7.4", &v1alpha1.PackageManifest{Name: "D"})
+					})
 
 					It("should return a RESOLVABLE result with latest D", func(ctx context.Context) {
-						latestVersion = "1.7.4"
-						res, err := dm.Validate(ctx, pi.Status.Manifest)
+						res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 						Expect(err).ShouldNot(HaveOccurred())
 						Expect(res).ShouldNot(BeNil())
 						Expect(res.Status).Should(Equal(ValidationResultStatusResolvable))
@@ -454,13 +435,11 @@ 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")
 						e, ei = createPackageAndInfo("E", "11.80.0")
-						res, err := dm.Validate(ctx, pi.Status.Manifest)
+						res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 						Expect(err).ShouldNot(HaveOccurred())
 						Expect(res).ShouldNot(BeNil())
 						Expect(res.Status).Should(Equal(ValidationResultStatusOk))
@@ -470,9 +449,13 @@ var _ = Describe("Dependency Manager", func() {
 				})
 
 				When("D, E do not exist", func() {
+					BeforeEach(func() {
+						fakeRepo.AddPackage("D", "1.1.7", &v1alpha1.PackageManifest{Name: "D"})
+						fakeRepo.AddPackage("E", "1.1.7", &v1alpha1.PackageManifest{Name: "E"})
+					})
+
 					It("Should return RESOLVABLE result with D, E as requirements", func(ctx context.Context) {
-						latestVersion = "1.1.7"
-						res, err := dm.Validate(ctx, pi.Status.Manifest)
+						res, err := dm.Validate(ctx, pi.Status.Manifest, p.Spec.PackageInfo.Version)
 						Expect(err).ShouldNot(HaveOccurred())
 						Expect(res).ShouldNot(BeNil())
 						Expect(res.Status).Should(Equal(ValidationResultStatusResolvable))
@@ -501,78 +484,6 @@ var _ = Describe("Dependency Manager", func() {
 
 	Describe("CheckConflict", func() {
 
-		DescribeTable("Checking version ranges",
-			func(versionRange string, version string, valid bool) {
-				conflict, err := dm.CheckConflict(version, v1alpha1.Dependency{Version: versionRange})
-				if valid {
-					Expect(conflict).To(BeNil())
-					Expect(err).NotTo(HaveOccurred())
-				} else {
-					Expect(conflict).NotTo(BeNil())
-					Expect(err).NotTo(HaveOccurred())
-				}
-			},
-			Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.0", true),
-			Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.0+0", true),
-			Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.4", true),
-			Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.9", true),
-			Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.1.9", false),
-			Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.0-rc.1", false),
-			Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.3.0-alpha.1", false),
-			Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.3.0", false),
-
-			Entry("When minor version is pinned with ~", "~1.2.0", "1.2.0", true),
-			Entry("When minor version is pinned with ~", "~1.2.0", "1.2.0+0", true),
-			Entry("When minor version is pinned with ~", "~1.2.0", "1.2.4", true),
-			Entry("When minor version is pinned with ~", "~1.2.0", "1.2.9", true),
-			Entry("When minor version is pinned with ~", "~1.2.0", "1.1.9", false),
-			Entry("When minor version is pinned with ~", "~1.2.0", "1.2.0-rc.1", false),
-			Entry("When minor version is pinned with ~", "~1.2.0", "1.3.0-alpha.1", false),
-			Entry("When minor version is pinned with ~", "~1.2.0", "1.3.0", false),
-
-			Entry("When minor version is pinned with ~", "~1.2", "1.2.0", true),
-			Entry("When minor version is pinned with ~", "~1.2", "1.2.0+0", true),
-			Entry("When minor version is pinned with ~", "~1.2", "1.2.4", true),
-			Entry("When minor version is pinned with ~", "~1.2", "1.2.9", true),
-			Entry("When minor version is pinned with ~", "~1.2", "1.1.9", false),
-			Entry("When minor version is pinned with ~", "~1.2", "1.2.0-rc.1", false),
-			Entry("When minor version is pinned with ~", "~1.2", "1.3.0-alpha.1", false),
-			Entry("When minor version is pinned with ~", "~1.2", "1.3.0", false),
-
-			Entry("When minor version is pinned with .x", "1.2.x", "1.2.0", true),
-			Entry("When minor version is pinned with .x", "1.2.x", "1.2.0+0", true),
-			Entry("When minor version is pinned with .x", "1.2.x", "1.2.4", true),
-			Entry("When minor version is pinned with .x", "1.2.x", "1.2.9", true),
-			Entry("When minor version is pinned with .x", "1.2.x", "1.1.9", false),
-			Entry("When minor version is pinned with .x", "1.2.x", "1.2.0-rc.1", false),
-			Entry("When minor version is pinned with .x", "1.2.x", "1.3.0-alpha.1", false),
-			Entry("When minor version is pinned with .x", "1.2.x", "1.3.0", false),
-
-			Entry("When major version is pinned with ^", "^1", "1.0.0", true),
-			Entry("When major version is pinned with ^", "^1", "1.3.5", true),
-			Entry("When major version is pinned with ^", "^1", "1.7.9+1", true),
-			Entry("When major version is pinned with ^", "^1", "1.0.0-rc.1", false),
-			Entry("When major version is pinned with ^", "^1", "2.0.0-rc.1", false),
-			Entry("When major version is pinned with ^", "^1", "2.0.0", false),
-
-			Entry("When major version is pinned with .x", "1.x", "1.0.0", true),
-			Entry("When major version is pinned with .x", "1.x", "1.3.5", true),
-			Entry("When major version is pinned with .x", "1.x", "1.7.9+1", true),
-			Entry("When major version is pinned with .x", "1.x", "1.0.0-rc.1", false),
-			Entry("When major version is pinned with .x", "1.x", "2.0.0-rc.1", false),
-			Entry("When major version is pinned with .x", "1.x", "2.0.0", false),
-
-			Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0-alpha.1", true),
-			Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0-beta.0", true),
-			Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0-rc.0", true),
-			Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0+0", true),
-			Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "3.0.0", true),
-			Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0-alpha.0", false),
-
-			Entry("When a minimum contains a build number", ">= 1.0.0+3", "1.0.0+3", true),
-			Entry("When a minimum contains a build number", ">= 1.0.0+3", "1.0.0+2", true), // TODO to be fixed with #405
-		)
-
 	})
 
 })
diff --git a/internal/semver/validate_test.go b/internal/semver/validate_test.go
new file mode 100644
index 0000000..f3f394c
--- /dev/null
+++ b/internal/semver/validate_test.go
@@ -0,0 +1,79 @@
+package semver
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("ValidateConstraint", func() {
+	DescribeTable("Checking version ranges",
+		func(versionRange string, version string, valid bool) {
+			err := ValidateConstraint(version, versionRange)
+			if valid {
+				Expect(err).NotTo(HaveOccurred())
+			} else {
+				Expect(err).To(HaveOccurred())
+				Expect(err).To(MatchError((*ConstraintValidationError)(nil)))
+			}
+		},
+		Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.0", true),
+		Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.0+0", true),
+		Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.4", true),
+		Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.9", true),
+		Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.1.9", false),
+		Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.2.0-rc.1", false),
+		Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.3.0-alpha.1", false),
+		Entry("When minor version is pinned", ">=1.2.0 <1.3.0", "1.3.0", false),
+
+		Entry("When minor version is pinned with ~", "~1.2.0", "1.2.0", true),
+		Entry("When minor version is pinned with ~", "~1.2.0", "1.2.0+0", true),
+		Entry("When minor version is pinned with ~", "~1.2.0", "1.2.4", true),
+		Entry("When minor version is pinned with ~", "~1.2.0", "1.2.9", true),
+		Entry("When minor version is pinned with ~", "~1.2.0", "1.1.9", false),
+		Entry("When minor version is pinned with ~", "~1.2.0", "1.2.0-rc.1", false),
+		Entry("When minor version is pinned with ~", "~1.2.0", "1.3.0-alpha.1", false),
+		Entry("When minor version is pinned with ~", "~1.2.0", "1.3.0", false),
+
+		Entry("When minor version is pinned with ~", "~1.2", "1.2.0", true),
+		Entry("When minor version is pinned with ~", "~1.2", "1.2.0+0", true),
+		Entry("When minor version is pinned with ~", "~1.2", "1.2.4", true),
+		Entry("When minor version is pinned with ~", "~1.2", "1.2.9", true),
+		Entry("When minor version is pinned with ~", "~1.2", "1.1.9", false),
+		Entry("When minor version is pinned with ~", "~1.2", "1.2.0-rc.1", false),
+		Entry("When minor version is pinned with ~", "~1.2", "1.3.0-alpha.1", false),
+		Entry("When minor version is pinned with ~", "~1.2", "1.3.0", false),
+
+		Entry("When minor version is pinned with .x", "1.2.x", "1.2.0", true),
+		Entry("When minor version is pinned with .x", "1.2.x", "1.2.0+0", true),
+		Entry("When minor version is pinned with .x", "1.2.x", "1.2.4", true),
+		Entry("When minor version is pinned with .x", "1.2.x", "1.2.9", true),
+		Entry("When minor version is pinned with .x", "1.2.x", "1.1.9", false),
+		Entry("When minor version is pinned with .x", "1.2.x", "1.2.0-rc.1", false),
+		Entry("When minor version is pinned with .x", "1.2.x", "1.3.0-alpha.1", false),
+		Entry("When minor version is pinned with .x", "1.2.x", "1.3.0", false),
+
+		Entry("When major version is pinned with ^", "^1", "1.0.0", true),
+		Entry("When major version is pinned with ^", "^1", "1.3.5", true),
+		Entry("When major version is pinned with ^", "^1", "1.7.9+1", true),
+		Entry("When major version is pinned with ^", "^1", "1.0.0-rc.1", false),
+		Entry("When major version is pinned with ^", "^1", "2.0.0-rc.1", false),
+		Entry("When major version is pinned with ^", "^1", "2.0.0", false),
+
+		Entry("When major version is pinned with .x", "1.x", "1.0.0", true),
+		Entry("When major version is pinned with .x", "1.x", "1.3.5", true),
+		Entry("When major version is pinned with .x", "1.x", "1.7.9+1", true),
+		Entry("When major version is pinned with .x", "1.x", "1.0.0-rc.1", false),
+		Entry("When major version is pinned with .x", "1.x", "2.0.0-rc.1", false),
+		Entry("When major version is pinned with .x", "1.x", "2.0.0", false),
+
+		Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0-alpha.1", true),
+		Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0-beta.0", true),
+		Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0-rc.0", true),
+		Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0+0", true),
+		Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "3.0.0", true),
+		Entry("When a prerelease is the minimum", ">= 1.0.0-alpha.1", "1.0.0-alpha.0", false),
+
+		Entry("When a minimum contains a build number", ">= 1.0.0+3", "1.0.0+3", true),
+		Entry("When a minimum contains a build number", ">= 1.0.0+3", "1.0.0+2", true), // TODO to be fixed with #405
+	)
+})
diff --git a/internal/webhook/package_validator_test.go b/internal/webhook/package_validator_test.go
index 039e25d..455b1bf 100644
--- a/internal/webhook/package_validator_test.go
+++ b/internal/webhook/package_validator_test.go
@@ -29,8 +29,10 @@ func newPackageValidatingWebhook(objects ...client.Object) *PackageValidatingWeb
 	return &PackageValidatingWebhook{
 		Client:       fakeClient,
 		OwnerManager: ownerManager,
-		DependendcyManager: dependency.NewDependencyManager(ctrladapter.NewControllerRuntimeAdapter(fakeClient), ownerManager).
-			WithRepo(&fakeRepo),
+		DependendcyManager: dependency.NewDependencyManager(
+			ctrladapter.NewControllerRuntimeAdapter(fakeClient),
+			ownerManager,
+		).WithRepo(&fakeRepo),
 		repo: &fakeRepo,
 	}
 }
@@ -38,28 +40,63 @@ func newPackageValidatingWebhook(objects ...client.Object) *PackageValidatingWeb
 var (
 	foov1pkg = v1alpha1.Package{
 		ObjectMeta: v1.ObjectMeta{Name: "foo"},
-		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "foo", Version: "v1"}}}
+		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"},
+		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{
 		ObjectMeta: v1.ObjectMeta{Name: "foo"},
-		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "foo", Version: "v2"}}}
+		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"},
+		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{
 		ObjectMeta: v1.ObjectMeta{Name: "bar"},
-		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "bar", Version: "v1"}}}
+		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"},
+		Spec:       v1alpha1.PackageInfoSpec{Name: "bar", Version: "v1"},
+		Status:     v1alpha1.PackageInfoStatus{Manifest: &v1alpha1.PackageManifest{Name: "bar"}}}
 	barv2pkg = v1alpha1.Package{
 		ObjectMeta: v1.ObjectMeta{Name: "bar"},
-		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "bar", Version: "v2"}}}
+		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"},
+		Spec:       v1alpha1.PackageInfoSpec{Name: "bar", Version: "v2"},
+		Status:     v1alpha1.PackageInfoStatus{Manifest: &v1alpha1.PackageManifest{Name: "bar"}}}
 	bazv1pkg = v1alpha1.Package{
 		ObjectMeta: v1.ObjectMeta{Name: "baz"},
-		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "baz", Version: "v1"}}}
+		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"},
+		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{
 		ObjectMeta: v1.ObjectMeta{Name: "baz"},
-		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "baz", Version: "v2"}}}
+		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"},
+		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{
 		ObjectMeta: v1.ObjectMeta{Name: "doesnotexist"},
 		Spec:       v1alpha1.PackageSpec{PackageInfo: v1alpha1.PackageInfoTemplate{Name: "doesnotexist", Version: "v1"}}}
 )
 
-// These tests demonstrate how unit tests for a CustomValidator COULD be implemented using the controller-runtime fake client.
+// These tests demonstrate how unit tests for a CustomValidator COULD be implemented using the controller-runtime
+// fake client.
 // They will likely become useless once the PackageValidatingWebhook does some actual validation.
 // TODO: Add some tests for package validation
 
@@ -67,18 +104,18 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 	BeforeAll(func() {
 		err := v1alpha1.AddToScheme(scheme.Scheme)
 		Expect(err).NotTo(HaveOccurred())
-		fakeRepo.Packages = map[string]map[string]v1alpha1.PackageManifest{
+		fakeRepo.Packages = map[string]map[string]*v1alpha1.PackageManifest{
 			"foo": {
-				"v1": {Dependencies: []v1alpha1.Dependency{{Name: "bar", Version: "v1"}}},
-				"v2": {Dependencies: []v1alpha1.Dependency{{Name: "bar", Version: "v2"}}},
+				"v1": foov1pi.Status.Manifest,
+				"v2": foov2pi.Status.Manifest,
 			},
 			"bar": {
-				"v1": {},
-				"v2": {},
+				"v1": barv1pi.Status.Manifest,
+				"v2": barv2pi.Status.Manifest,
 			},
 			"baz": {
-				"v1": {Dependencies: []v1alpha1.Dependency{{Name: "foo", Version: "v1"}}},
-				"v2": {Dependencies: []v1alpha1.Dependency{{Name: "foo", Version: "v2"}}},
+				"v1": bazv1pi.Status.Manifest,
+				"v2": bazv2pi.Status.Manifest,
 			},
 		}
 	})
@@ -101,7 +138,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 		When("the dependency exists", func() {
 			When("dependency version ok", func() {
 				It("should not return error", func(ctx context.Context) {
-					webhook := newPackageValidatingWebhook(&barv1pkg)
+					webhook := newPackageValidatingWebhook(&barv1pkg, &barv1pi)
 					_, err := webhook.ValidateCreate(ctx,
 						&foov1pkg)
 					Expect(err).NotTo(HaveOccurred())
@@ -109,7 +146,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 			})
 			When("there is a version mismatch", func() {
 				It("should return conflict error", func(ctx context.Context) {
-					webhook := newPackageValidatingWebhook(&barv1pkg)
+					webhook := newPackageValidatingWebhook(&barv1pkg, &barv1pi)
 					_, err := webhook.ValidateCreate(ctx, &foov2pkg)
 					Expect(err).To(And(HaveOccurred(), Satisfy(isErrDependencyConflict)))
 				})
@@ -119,7 +156,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 			It("should return error", func(ctx context.Context) {
 				webhook := newPackageValidatingWebhook()
 				_, err := webhook.ValidateCreate(ctx, &bazv1pkg)
-				Expect(err).To(And(HaveOccurred(), Satisfy(isErrTransitiveDependency)))
+				Expect(err).NotTo(HaveOccurred())
 			})
 		})
 		When("called with object other than Package", func() {
@@ -142,7 +179,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 		When("the dependency exists", func() {
 			When("dependency version ok", func() {
 				It("should not return error", func(ctx context.Context) {
-					webhook := newPackageValidatingWebhook(&barv2pkg)
+					webhook := newPackageValidatingWebhook(&barv2pkg, &barv2pi)
 					_, err := webhook.ValidateUpdate(ctx,
 						&foov1pkg, &foov2pkg)
 					Expect(err).NotTo(HaveOccurred())
@@ -150,7 +187,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 			})
 			When("there is a version mismatch", func() {
 				It("should return conflict error", func(ctx context.Context) {
-					webhook := newPackageValidatingWebhook(&barv1pkg)
+					webhook := newPackageValidatingWebhook(&barv1pkg, &barv1pi)
 					_, err := webhook.ValidateUpdate(ctx, &foov1pkg, &foov2pkg)
 					Expect(err).To(And(HaveOccurred(), Satisfy(isErrDependencyConflict)))
 				})
@@ -160,7 +197,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 			It("should return error", func(ctx context.Context) {
 				webhook := newPackageValidatingWebhook()
 				_, err := webhook.ValidateUpdate(ctx, &bazv1pkg, &bazv2pkg)
-				Expect(err).To(And(HaveOccurred(), Satisfy(isErrTransitiveDependency)))
+				Expect(err).NotTo(HaveOccurred())
 			})
 		})
 		When("called with object other than Package", func() {
@@ -175,7 +212,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 	Context("ValidateDelete", func() {
 		When("package has no owner references", func() {
 			It("should not return error", func(ctx context.Context) {
-				webhook := newPackageValidatingWebhook(&foov1pkg)
+				webhook := newPackageValidatingWebhook(&foov1pkg, &foov1pi)
 				_, err := webhook.ValidateDelete(ctx, &foov1pkg)
 				Expect(err).NotTo(HaveOccurred())
 			})
@@ -186,7 +223,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 				err := controllerutil.SetOwnerReference(&foov1pkg, barv1Copy, scheme.Scheme)
 				Expect(err).NotTo(HaveOccurred())
 
-				webhook := newPackageValidatingWebhook(&foov1pkg, barv1Copy)
+				webhook := newPackageValidatingWebhook(&foov1pkg, &foov1pi, barv1Copy, &barv1pi)
 				_, err = webhook.ValidateDelete(ctx, barv1Copy)
 				Expect(err).To(And(HaveOccurred(), Satisfy(isErrDependencyConflict)))
 			})
@@ -199,7 +236,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 					err := controllerutil.SetOwnerReference(foov1Copy, barv1Copy, scheme.Scheme)
 					Expect(err).NotTo(HaveOccurred())
 
-					webhook := newPackageValidatingWebhook(foov1Copy, barv1Copy)
+					webhook := newPackageValidatingWebhook(foov1Copy, &foov1pi, barv1Copy, &barv1pi)
 					_, err = webhook.ValidateDelete(ctx, barv1Copy)
 					Expect(err).NotTo(HaveOccurred())
 				})