Repository: glasskube/glasskube
Pull request #371 (2024-03-14 11:44:13)

fix(cli, ui): add support for bootstrap latest via ui, implicit request
## 📑 Description
This PR adds the "bootstrap latest" functionality to all other places where we bootstrap, aside from the `bootstrap` command.

## ✅ Checks
- [ ] My code requires changes to the documentation
- [ ] I have updated the documentation as required

## ℹ Additional Information
This is a follow-up of #361.


Patch:
diff --git a/Makefile b/Makefile
index aaec84c..c8bf11e 100644
--- a/Makefile
+++ b/Makefile
@@ -4,11 +4,15 @@ IMG ?= ghcr.io/glasskube/package-operator:snapshot
 # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
 ENVTEST_K8S_VERSION = 1.28.0
 
+# Allows to override name and location of the "go" binary.
+# This allows using different versions of Go, as outlined in the documentation: https://go.dev/doc/manage-install
+GOCMD?=go
+
 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
-ifeq (,$(shell go env GOBIN))
-GOBIN=$(shell go env GOPATH)/bin
+ifeq (,$(shell $(GOCMD) env GOBIN))
+GOBIN=$(shell $(GOCMD) env GOPATH)/bin
 else
-GOBIN=$(shell go env GOBIN)
+GOBIN=$(shell $(GOCMD) env GOBIN)
 endif
 
 # CONTAINER_TOOL defines the container tool to be used for building images.
@@ -48,7 +52,7 @@ help: ## Display this help.
 
 .PHONY: schema-gen
 schema-gen: # Generate a schema.json file for the package manifest type.
-	go run ./internal/cmd/schema-gen
+	$(GOCMD) run ./internal/cmd/schema-gen
 
 .PHONY: manifests
 manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
@@ -60,15 +64,15 @@ generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and
 
 .PHONY: tidy
 tidy: ## Run go mod tidy
-	go mod tidy
+	$(GOCMD) mod tidy
 
 .PHONY: fmt
 fmt: ## Run go fmt against code.
-	go fmt ./...
+	$(GOCMD) fmt ./...
 
 .PHONY: vet
 vet: ## Run go vet against code.
-	go vet ./...
+	$(GOCMD) vet ./...
 
 .PHONY: lint
 lint: lint-go lint-web
@@ -78,7 +82,7 @@ lint-fix: lint-go-fix lint-web-fix
 
 .PHONY: test
 test: manifests generate fmt vet envtest ## Run tests.
-	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
+	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GOCMD) test ./... -coverprofile cover.out
 
 GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint
 GOLANGCI_LINT_VERSION ?= v1.54.2
@@ -100,23 +104,23 @@ lint-go-fix: golangci-lint ## Run golangci-lint linter and perform fixes
 
 .PHONY: build
 build: manifests generate fmt vet lint-go ## Build manager binary.
-	go build -o $(OUT_DIR)/package-operator ./cmd/package-operator/
+	$(GOCMD) build -o $(OUT_DIR)/package-operator ./cmd/package-operator/
 
 .PHONY: build-cert
 build-cert: manifests generate fmt vet lint-go ## Build manager binary.
-	go build -o $(OUT_DIR)/cert-manager ./cmd/cert-manager/
+	$(GOCMD) build -o $(OUT_DIR)/cert-manager ./cmd/cert-manager/
 
 .PHONY: build-cli
 build-cli: fmt vet lint-go lint-web web ## Build cli binary.
-	go build -o $(OUT_DIR)/glasskube ./cmd/glasskube/
+	$(GOCMD) build -o $(OUT_DIR)/glasskube ./cmd/glasskube/
 
 .PHONY: run
 run: manifests generate fmt vet ## Run a controller from your host.
-	go run ./cmd/package-operator/
+	$(GOCMD) run ./cmd/package-operator/
 
 .PHONY: cert
 cert: manifests generate fmt vet
-	go run ./cmd/cert-manager/ --cert-dir /tmp/k8s-webhook-server/serving-certs
+	$(GOCMD) run ./cmd/cert-manager/ --cert-dir /tmp/k8s-webhook-server/serving-certs
 
 # If you wish to build the manager image targeting other platforms you can use the --platform flag.
 # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
@@ -225,15 +229,15 @@ $(KUSTOMIZE): $(LOCALBIN)
 		echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \
 		rm -rf $(LOCALBIN)/kustomize; \
 	fi
-	test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION)
+	test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on $(GOCMD) install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION)
 
 .PHONY: controller-gen
 controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten.
 $(CONTROLLER_GEN): $(LOCALBIN)
 	test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \
-	GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
+	GOBIN=$(LOCALBIN) $(GOCMD) install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION)
 
 .PHONY: envtest
 envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
 $(ENVTEST): $(LOCALBIN)
-	test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
+	test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) $(GOCMD) install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
diff --git a/api/v1alpha1/owned_resource.go b/api/v1alpha1/owned_resource.go
index 7e6fe4d..ecf90d5 100644
--- a/api/v1alpha1/owned_resource.go
+++ b/api/v1alpha1/owned_resource.go
@@ -8,6 +8,7 @@ type OwnedResourceRef struct {
 	metav1.GroupVersionKind `json:",inline"`
 	Name                    string `json:"name"`
 	Namespace               string `json:"namespace,omitempty"`
+	MarkedForDeletion       bool   `json:"markedForDeletion,omitempty"`
 }
 
 func (orr OwnedResourceRef) String() string {
diff --git a/cmd/glasskube/cmd/bootstrap.go b/cmd/glasskube/cmd/bootstrap.go
index fdba13d..0e4b8b3 100644
--- a/cmd/glasskube/cmd/bootstrap.go
+++ b/cmd/glasskube/cmd/bootstrap.go
@@ -29,36 +29,27 @@ var bootstrapCmd = &cobra.Command{
 	PreRun: cliutils.SetupClientContext(false),
 	Run: func(cmd *cobra.Command, args []string) {
 		cfg, _ := cliutils.RequireConfig(config.Kubeconfig)
-
-		if bootstrapCmdOptions.latest {
-			if releaseInfo, err := cliutils.FetchLatestRelease(); err != nil {
-				fmt.Fprintln(os.Stderr, "No updates available, bootstrapping with currently installed version.")
-			} else {
-				bootstrapCmdOptions.url = fmt.Sprintf(
-					"https://github.com/glasskube/glasskube/releases/download/v%s/manifest-aio.yaml",
-					releaseInfo.Version,
-				)
-			}
-		}
-
-		client := bootstrap.NewBootstrapClient(
-			cfg,
-			bootstrapCmdOptions.url,
-			cmd.Root().Version,
-			bootstrapCmdOptions.bootstrapType,
-		)
-		if err := client.Bootstrap(cmd.Context()); err != nil {
+		client := bootstrap.NewBootstrapClient(cfg)
+		if err := client.Bootstrap(cmd.Context(), bootstrapCmdOptions.asBootstrapOptions()); err != nil {
 			fmt.Fprintf(os.Stderr, "\nAn error occurred during bootstrap:\n%v\n", err)
 			os.Exit(1)
 		}
 	},
 }
 
+func (o bootstrapOptions) asBootstrapOptions() bootstrap.BootstrapOptions {
+	return bootstrap.BootstrapOptions{
+		Type:   o.bootstrapType,
+		Url:    o.url,
+		Latest: o.latest,
+	}
+}
+
 func init() {
 	RootCmd.AddCommand(bootstrapCmd)
 	bootstrapCmd.Flags().StringVarP(&bootstrapCmdOptions.url, "url", "u", "", "URL to fetch the Glasskube operator from")
 	bootstrapCmd.Flags().VarP(&bootstrapCmdOptions.bootstrapType, "type", "t", `Type of manifest to use for bootstrapping`)
-	bootstrapCmd.Flags().BoolVar(&bootstrapCmdOptions.latest, "latest", config.Version == "dev",
+	bootstrapCmd.Flags().BoolVar(&bootstrapCmdOptions.latest, "latest", config.IsDevBuild(),
 		"Fetch and bootstrap the latest version")
 	bootstrapCmd.MarkFlagsMutuallyExclusive("url", "type")
 	bootstrapCmd.MarkFlagsMutuallyExclusive("url", "latest")
diff --git a/cmd/glasskube/cmd/install.go b/cmd/glasskube/cmd/install.go
index e61e1b8..c20c43f 100644
--- a/cmd/glasskube/cmd/install.go
+++ b/cmd/glasskube/cmd/install.go
@@ -5,6 +5,9 @@ import (
 	"os"
 	"strings"
 
+	"github.com/glasskube/glasskube/internal/controller/owners"
+	"k8s.io/client-go/kubernetes/scheme"
+
 	"github.com/fatih/color"
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/cliutils"
@@ -35,7 +38,8 @@ var installCmd = &cobra.Command{
 		ctx := cmd.Context()
 		config := client.RawConfigFromContext(ctx)
 		pkgClient := client.FromContext(ctx)
-		dm := dependency.NewDependencyManager(clientadapter.NewGoClientAdapter(pkgClient))
+		dm := dependency.NewDependencyManager(clientadapter.NewGoClientAdapter(pkgClient),
+			owners.NewOwnerManager(scheme.Scheme))
 		installer := install.NewInstaller(pkgClient).WithStatusWriter(statuswriter.Spinner())
 		bold := color.New(color.Bold).SprintFunc()
 		packageName := args[0]
diff --git a/config/crd/bases/packages.glasskube.dev_packages.yaml b/config/crd/bases/packages.glasskube.dev_packages.yaml
index 12b6130..7c73d1c 100644
--- a/config/crd/bases/packages.glasskube.dev_packages.yaml
+++ b/config/crd/bases/packages.glasskube.dev_packages.yaml
@@ -146,6 +146,8 @@ spec:
                       type: string
                     kind:
                       type: string
+                    markedForDeletion:
+                      type: boolean
                     name:
                       type: string
                     namespace:
@@ -166,6 +168,8 @@ spec:
                       type: string
                     kind:
                       type: string
+                    markedForDeletion:
+                      type: boolean
                     name:
                       type: string
                     namespace:
@@ -186,6 +190,8 @@ spec:
                       type: string
                     kind:
                       type: string
+                    markedForDeletion:
+                      type: boolean
                     name:
                       type: string
                     namespace:
diff --git a/internal/cliutils/checkforupdate.go b/internal/cliutils/checkforupdate.go
index 630d3d5..6d08b57 100644
--- a/internal/cliutils/checkforupdate.go
+++ b/internal/cliutils/checkforupdate.go
@@ -1,70 +1,34 @@
 package cliutils
 
 import (
-	"encoding/json"
 	"fmt"
-	"net/http"
 	"os"
-	"regexp"
-	"strconv"
 
+	"github.com/Masterminds/semver/v3"
 	"github.com/glasskube/glasskube/internal/config"
+	"github.com/glasskube/glasskube/internal/releaseinfo"
 )
 
-type ReleaseInfo struct {
-	Version string `json:"version"`
-}
-
-func versionIsGreater(version1, version2 string) bool {
-	r := regexp.MustCompile(`(\d+)`)
-	v1 := r.FindAllString(version1, -1)
-	v2 := r.FindAllString(version2, -1)
-
-	for i := 0; i < len(v1) && i < len(v2); i++ {
-		num1, _ := strconv.Atoi(v1[i])
-		num2, _ := strconv.Atoi(v2[i])
-
-		if num1 < num2 {
-			return true
-		} else if num1 > num2 {
-			return false
-		}
-	}
-
-	return len(v1) < len(v2)
-}
-
-func FetchLatestRelease() (*ReleaseInfo, error) {
-	url := "https://glasskube.dev/release.json"
-
-	resp, _ := http.Get(url)
-
-	defer func() {
-		_ = resp.Body.Close()
-	}()
-
-	var releaseInfo ReleaseInfo
-	if err := json.NewDecoder(resp.Body).Decode(&releaseInfo); err != nil {
+// CheckForUpdate determines the new version by fetching the latest release info.
+// If no newer version exists, it returns nil.
+func CheckForUpdate() (*string, error) {
+	if releaseInfo, err := releaseinfo.FetchLatestRelease(); err != nil {
 		return nil, err
+	} else if version, err := semver.NewVersion(config.Version); err != nil {
+		return nil, err
+	} else if latestVersion, err := semver.NewVersion(releaseInfo.Version); err != nil {
+		return nil, err
+	} else if latestVersion.GreaterThan(version) {
+		return &releaseInfo.Version, nil
+	} else {
+		return nil, nil
 	}
-
-	return &releaseInfo, nil
-}
-
-func CheckForUpdate() (bool, string) {
-	if releaseInfo, err := FetchLatestRelease(); err != nil {
-		return false, ""
-	} else if versionIsGreater(config.Version, releaseInfo.Version) {
-		return true, releaseInfo.Version
-	}
-
-	return false, ""
 }
 
 func UpdateFetch() {
-	updateAvailable, latestVersion := CheckForUpdate()
-	if updateAvailable {
-		fmt.Fprintf(os.Stderr, "📣 A newer version of Glasskube is available: %s → %s\n", config.Version, latestVersion)
-		fmt.Fprintf(os.Stderr, "📘 Release notes: https://github.com/glasskube/glasskube/releases/tag/v%v\n\n", latestVersion)
+	newerVersion, err := CheckForUpdate()
+	if err == nil && newerVersion != nil {
+		fmt.Fprintf(os.Stderr, "📣 A newer version of Glasskube is available: %s → %s\n", config.Version, *newerVersion)
+		fmt.Fprintf(os.Stderr, "📘 Release notes: https://github.com/glasskube/glasskube/releases/tag/v%v\n\n", *newerVersion)
 	}
 }
diff --git a/internal/cliutils/setupclientcontext.go b/internal/cliutils/setupclientcontext.go
index d5ad23a..d7fe904 100644
--- a/internal/cliutils/setupclientcontext.go
+++ b/internal/cliutils/setupclientcontext.go
@@ -55,13 +55,8 @@ func RequireBootstrapped(ctx context.Context, cfg *rest.Config, rawCfg *api.Conf
 			fmt.Fprint(os.Stderr, "Execution cancelled – Glasskube is not yet bootstrapped.\n")
 			os.Exit(1)
 		}
-		client := bootstrap.NewBootstrapClient(
-			cfg,
-			"",
-			config.Version,
-			bootstrap.BootstrapTypeAio,
-		)
-		if err := client.Bootstrap(ctx); err != nil {
+		client := bootstrap.NewBootstrapClient(cfg)
+		if err := client.Bootstrap(ctx, bootstrap.DefaultOptions()); err != nil {
 			fmt.Fprintf(os.Stderr, "\nAn error occurred during bootstrap:\n%v\n", err)
 			os.Exit(1)
 		} else {
diff --git a/internal/controller/owners/owners.go b/internal/controller/owners/owners.go
index abaa982..6360a2d 100644
--- a/internal/controller/owners/owners.go
+++ b/internal/controller/owners/owners.go
@@ -31,6 +31,10 @@ func NewOwnerManager(scheme *runtime.Scheme) *OwnerManager {
 	return &OwnerManager{scheme: scheme}
 }
 
+func (mgr *OwnerManager) GetScheme() *runtime.Scheme {
+	return mgr.scheme
+}
+
 func (mgr *OwnerManager) HasOwner(owner client.Object, obj metav1.Object) (bool, error) {
 	if _, err := mgr.findOwnerReferenceIndex(owner, obj.GetOwnerReferences()); err != nil && !errors.Is(err, ErrNoSuchOwner) {
 		return false, err
diff --git a/internal/controller/owners/utils/owned_resources.go b/internal/controller/owners/utils/owned_resources.go
index f0f583d..6cbe08f 100644
--- a/internal/controller/owners/utils/owned_resources.go
+++ b/internal/controller/owners/utils/owned_resources.go
@@ -44,6 +44,16 @@ func Remove(refs *[]packagesv1alpha1.OwnedResourceRef, toRemove packagesv1alpha1
 	return false
 }
 
+func MarkForDeletion(refs *[]packagesv1alpha1.OwnedResourceRef, toRemove packagesv1alpha1.OwnedResourceRef) bool {
+	for i, ref := range *refs {
+		if RefersToSameResource(ref, toRemove) {
+			(*refs)[i].MarkedForDeletion = true
+			return true
+		}
+	}
+	return false
+}
+
 func AddOwnedResourceRef(
 	scheme *runtime.Scheme,
 	refs *[]packagesv1alpha1.OwnedResourceRef,
diff --git a/internal/controller/package_controller.go b/internal/controller/package_controller.go
index 0ba2dfc..02371a7 100644
--- a/internal/controller/package_controller.go
+++ b/internal/controller/package_controller.go
@@ -104,7 +104,7 @@ func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		r.OwnerManager = owners.NewOwnerManager(r.Scheme)
 	}
 	if r.dependencyMgr == nil {
-		r.dependencyMgr = dependency.NewDependencyManager(controllerruntime.NewControllerRuntimeAdapter(r.Client))
+		r.dependencyMgr = dependency.NewDependencyManager(controllerruntime.NewControllerRuntimeAdapter(r.Client), r.OwnerManager)
 	}
 	controllerBuilder := ctrl.NewControllerManagedBy(mgr).For(&packagesv1alpha1.Package{})
 	for _, adapter := range []manifest.ManifestAdapter{r.HelmAdapter, r.KustomizeAdapter, r.ManifestAdapter} {
@@ -583,32 +583,37 @@ OuterLoop:
 		} else if owning, err := r.HasOwner(r.pkg, &oldReqPkg); err != nil {
 			log.Error(err, "Failed to check owner references of old required package", "oldPackage", ref.Name)
 		} else if owning {
-			var cnt int
-			cnt, err = r.CountOwnersOfType(r.pkg, &oldReqPkg)
-			if err != nil {
-				log.Error(err, "Failed to check owner references of old required package", "oldPackage", ref.Name)
-				continue
-			}
-			if err := r.RemoveOwner(r.pkg, &oldReqPkg); err != nil {
-				log.Error(err, "Failed to remove owner reference", "oldPackage", ref.Name)
-			} else if err := r.Update(ctx, &oldReqPkg); err != nil {
-				log.Error(err, "Failed to update old package with removed owner reference", "oldPackage", ref.Name)
-			} else {
-				r.setShouldUpdate(ownerutils.Remove(&ownedPackagesCopy, ref))
-				log.Info(fmt.Sprintf("Removed owner reference from %v to %v", r.pkg.Name, ref.Name))
-
-				if cnt == 1 {
-					// remove the old package if we were the only package owning it
-					deletePropagationForeground := metav1.DeletePropagationForeground
-					if err := r.Delete(ctx, ownerutils.OwnedResourceRefToObject(ref), &client.DeleteOptions{
-						PropagationPolicy: &deletePropagationForeground,
-					}); err != nil && !apierrors.IsNotFound(err) {
-						errs = multierr.Append(errs, fmt.Errorf("could not prune package: %w", err))
-					} else {
-						log.V(1).Info("pruned package", "reference", ref)
-						r.setShouldUpdate(ownerutils.Remove(&ownedPackagesCopy, ref))
+			if ref.MarkedForDeletion {
+				log.Info(fmt.Sprintf("reference from %v to %v marked for deletion will be removed", r.pkg.Name, ref.Name))
+				var cnt int
+				cnt, err = r.CountOwnersOfType(r.pkg, &oldReqPkg)
+				if err != nil {
+					log.Error(err, "Failed to check owner references of old required package", "oldPackage", ref.Name)
+					continue
+				}
+
+				if err := r.RemoveOwner(r.pkg, &oldReqPkg); err != nil {
+					log.Error(err, "Failed to remove owner reference", "oldPackage", ref.Name)
+				} else if err := r.Update(ctx, &oldReqPkg); err != nil {
+					log.Error(err, "Failed to update old package with removed owner reference", "oldPackage", ref.Name)
+				} else {
+					log.Info(fmt.Sprintf("Removed owner reference from %v to %v", r.pkg.Name, ref.Name))
+
+					if cnt == 1 {
+						// remove the old package if we were the only package owning it
+						deletePropagationForeground := metav1.DeletePropagationForeground
+						if err := r.Delete(ctx, ownerutils.OwnedResourceRefToObject(ref), &client.DeleteOptions{
+							PropagationPolicy: &deletePropagationForeground,
+						}); err != nil && !apierrors.IsNotFound(err) {
+							errs = multierr.Append(errs, fmt.Errorf("could not prune package: %w", err))
+						} else {
+							log.V(1).Info("pruned package", "reference", ref)
+						}
 					}
 				}
+			} else {
+				log.Info(fmt.Sprintf("marking for deletion: reference from %v to %v", r.pkg.Name, ref.Name))
+				r.setShouldUpdate(ownerutils.MarkForDeletion(&ownedPackagesCopy, ref))
 			}
 		} else {
 			r.setShouldUpdate(ownerutils.Remove(&ownedPackagesCopy, ref))
diff --git a/internal/dependency/adapter/adapter.go b/internal/dependency/adapter/adapter.go
index 0ec18af..4f1002c 100644
--- a/internal/dependency/adapter/adapter.go
+++ b/internal/dependency/adapter/adapter.go
@@ -8,6 +8,8 @@ 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 {
diff --git a/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go b/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go
index 8e4a37a..6e2e048 100644
--- a/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go
+++ b/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go
@@ -34,3 +34,22 @@ func (a *ControllerRuntimeAdapter) GetPackage(ctx context.Context, pkgName strin
 		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{
+		Name: pkgInfoName,
+	}, &pkgInfo); err != nil {
+		return nil, err
+	} else {
+		return &pkgInfo, nil
+	}
+}
+
+func (a *ControllerRuntimeAdapter) ListPackages(ctx context.Context) (*v1alpha1.PackageList, error) {
+	var pkgList v1alpha1.PackageList
+	if err := a.client.List(ctx, &pkgList, &client.ListOptions{}); err != nil {
+		return nil, err
+	}
+	return &pkgList, nil
+}
diff --git a/internal/dependency/adapter/goclient/goclientadapter.go b/internal/dependency/adapter/goclient/goclientadapter.go
index d66a802..339a113 100644
--- a/internal/dependency/adapter/goclient/goclientadapter.go
+++ b/internal/dependency/adapter/goclient/goclientadapter.go
@@ -30,3 +30,20 @@ func (a *GoClientAdapter) GetPackage(ctx context.Context, pkgName string) (*v1al
 		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 {
+		return nil, err
+	} else {
+		return &pkgInfo, nil
+	}
+}
+
+func (a *GoClientAdapter) ListPackages(ctx context.Context) (*v1alpha1.PackageList, error) {
+	var pkgList v1alpha1.PackageList
+	if err := a.pkgClient.Packages().GetAll(ctx, &pkgList); err != nil {
+		return nil, err
+	}
+	return &pkgList, nil
+}
diff --git a/internal/dependency/manager.go b/internal/dependency/manager.go
index 5e59719..b9bc606 100644
--- a/internal/dependency/manager.go
+++ b/internal/dependency/manager.go
@@ -7,6 +7,9 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/glasskube/glasskube/internal/controller/owners"
+	"github.com/glasskube/glasskube/internal/controller/owners/utils"
+
 	"github.com/glasskube/glasskube/internal/dependency/adapter"
 
 	"github.com/Masterminds/semver/v3"
@@ -18,6 +21,7 @@ import (
 type DependendcyManager struct {
 	clientAdapter adapter.ClientAdapter
 	repoAdapter   adapter.RepoAdapter
+	*owners.OwnerManager
 }
 
 type ValidationResultStatus string
@@ -96,10 +100,11 @@ func (a *defaultRepoAdapter) GetMaxVersionCompatibleWith(repo string, pkgName st
 	}
 }
 
-func NewDependencyManager(adapter adapter.ClientAdapter) *DependendcyManager {
+func NewDependencyManager(adapter adapter.ClientAdapter, ownerMgr *owners.OwnerManager) *DependendcyManager {
 	return &DependendcyManager{
 		clientAdapter: adapter,
 		repoAdapter:   &defaultRepoAdapter{repo: repo2.DefaultClient},
+		OwnerManager:  ownerMgr,
 	}
 }
 
@@ -126,7 +131,7 @@ func (dm *DependendcyManager) Validate(ctx context.Context, pkg *v1alpha1.Packag
 				requirements = append(requirements, *req)
 			}
 		} else if dependency.Version != "" {
-			if conflict, err := dm.checkConflict(requiredPkg.Spec.PackageInfo.Version, dependency); err != nil {
+			if conflict, err := dm.CheckConflict(requiredPkg.Spec.PackageInfo.Version, dependency); err != nil {
 				return nil, err
 			} else if conflict != nil {
 				conflicts = append(conflicts, *conflict)
@@ -147,7 +152,7 @@ func (dm *DependendcyManager) Validate(ctx context.Context, pkg *v1alpha1.Packag
 	}, nil
 }
 
-func (dm *DependendcyManager) checkConflict(existingVersionStr string, dependency v1alpha1.Dependency) (*Conflict, error) {
+func (dm *DependendcyManager) CheckConflict(existingVersionStr string, dependency v1alpha1.Dependency) (*Conflict, error) {
 	existingVersion, err := semver.NewVersion(existingVersionStr)
 	if err != nil {
 		return nil, err
@@ -191,3 +196,103 @@ func (dm *DependendcyManager) createRequirement(dependency v1alpha1.Dependency)
 	}
 	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 {
+			return nil, err
+		} else if c != nil {
+			conflicts = append(conflicts, *c)
+		}
+	}
+	return conflicts, nil
+}
+
+type GetDependentsOptions struct {
+	PackageList         *v1alpha1.PackageList
+	IncludeDependencies bool
+}
+
+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 {
+				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)
+				}
+			}
+		}
+	}
+
+	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]})
+		}
+	}
+
+	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 {
+		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
+		}
+	}
+	return nil, nil
+}
diff --git a/internal/releaseinfo/releaseinfo.go b/internal/releaseinfo/releaseinfo.go
new file mode 100644
index 0000000..546c4b4
--- /dev/null
+++ b/internal/releaseinfo/releaseinfo.go
@@ -0,0 +1,27 @@
+package releaseinfo
+
+import (
+	"encoding/json"
+	"net/http"
+)
+
+type ReleaseInfo struct {
+	Version string `json:"version"`
+}
+
+func FetchLatestRelease() (*ReleaseInfo, error) {
+	url := "https://glasskube.dev/release.json"
+
+	resp, _ := http.Get(url)
+
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	var releaseInfo ReleaseInfo
+	if err := json.NewDecoder(resp.Body).Decode(&releaseInfo); err != nil {
+		return nil, err
+	}
+
+	return &releaseInfo, nil
+}
diff --git a/internal/web/server.go b/internal/web/server.go
index 121d5ed..2950426 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/cliutils"
-	"github.com/glasskube/glasskube/internal/config"
 	"github.com/glasskube/glasskube/internal/repo"
 	"github.com/glasskube/glasskube/internal/web/components/pkg_detail_btns"
 	"github.com/glasskube/glasskube/internal/web/components/pkg_overview_btn"
@@ -364,13 +363,8 @@ func (s *server) supportPage(w http.ResponseWriter, r *http.Request) {
 func (s *server) bootstrapPage(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 	if r.Method == "POST" {
-		client := bootstrap.NewBootstrapClient(
-			s.restConfig,
-			"",
-			config.Version,
-			bootstrap.BootstrapTypeAio,
-		)
-		if err := client.Bootstrap(ctx); err != nil {
+		client := bootstrap.NewBootstrapClient(s.restConfig)
+		if err := client.Bootstrap(ctx, bootstrap.DefaultOptions()); err != nil {
 			fmt.Fprintf(os.Stderr, "\nAn error occurred during bootstrap:\n%v\n", err)
 			err := bootstrapPageTmpl.ExecuteTemplate(w, "bootstrap-failure", nil)
 			checkTmplError(err, "bootstrap-failure")
diff --git a/internal/web/templates/pages/bootstrap.html b/internal/web/templates/pages/bootstrap.html
index 9fdc2b3..cd73cb1 100644
--- a/internal/web/templates/pages/bootstrap.html
+++ b/internal/web/templates/pages/bootstrap.html
@@ -33,14 +33,14 @@
   <hr />
   <small>
     For further information on bootstrapping, please consult the docs:
-    <a href="https://glasskube.dev/docs/getting-started/bootstrap" class="text-dark" target="_blank">Bootstrap</a
+    <a href="https://glasskube.dev/docs/getting-started/bootstrap" class="text-reset" target="_blank">Bootstrap</a
     >.<br />
     If you want to test Glasskube locally, check out
-    <a class="text-dark" href="https://minikube.sigs.k8s.io/" target="_blank">minikube</a>.<br />
+    <a class="text-reset" href="https://minikube.sigs.k8s.io/" target="_blank">minikube</a>.<br />
     If you have any questions or encounter bugs, don't hesitate to reach out via
-    <a href="https://github.com/glasskube/glasskube" class="text-dark" target="_blank">Github</a>
+    <a href="https://github.com/glasskube/glasskube" class="text-reset" target="_blank">Github</a>
     or
-    <a href="https://discord.gg/SxH6KUCGH7" class="text-dark" target="_blank">Discord</a>.
+    <a href="https://discord.gg/SxH6KUCGH7" class="text-reset" target="_blank">Discord</a>.
   </small>
 {{ end }}
 
diff --git a/internal/webhook/errors.go b/internal/webhook/errors.go
index 008340f..cb2c8d7 100644
--- a/internal/webhook/errors.go
+++ b/internal/webhook/errors.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	"github.com/glasskube/glasskube/internal/dependency"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 var ErrInvalidObject = errors.New("validator called with unexpected object type")
@@ -17,8 +16,8 @@ func newConflictError(conflicts dependency.Conflicts) error {
 	return fmt.Errorf("%w: %v", ErrDependencyConflict, conflicts)
 }
 
-func newConflictErrorDelete(owner metav1.OwnerReference) error {
-	return fmt.Errorf("%w: dependency of %v", ErrDependencyConflict, owner.Name)
+func newConflictErrorDelete(owner string) error {
+	return fmt.Errorf("%w: dependency of %v", ErrDependencyConflict, owner)
 }
 
 func newTransitiveError(requirement dependency.PackageWithVersion, dependency v1alpha1.Dependency) error {
diff --git a/internal/webhook/package_validator.go b/internal/webhook/package_validator.go
index 48584e4..0448ed6 100644
--- a/internal/webhook/package_validator.go
+++ b/internal/webhook/package_validator.go
@@ -11,7 +11,6 @@ import (
 	repoclient "github.com/glasskube/glasskube/internal/repo/client"
 	"go.uber.org/multierr"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -31,7 +30,7 @@ var _ webhook.CustomValidator = &PackageValidatingWebhook{}
 
 func (p *PackageValidatingWebhook) SetupWithManager(mgr ctrl.Manager) error {
 	p.OwnerManager = owners.NewOwnerManager(p.Scheme())
-	p.DependendcyManager = dependency.NewDependencyManager(ctrladapter.NewControllerRuntimeAdapter(p.Client))
+	p.DependendcyManager = dependency.NewDependencyManager(ctrladapter.NewControllerRuntimeAdapter(p.Client), p.OwnerManager)
 	p.repo = repo.DefaultClient
 	return ctrl.NewWebhookManagedBy(mgr).
 		For(&v1alpha1.Package{}).
@@ -78,15 +77,12 @@ 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.OwnersOfType(&v1alpha1.Package{}, pkg); err != nil {
+		if dependants, err := p.GetDependents(ctx, pkg, dependency.GetDependentsOptions{IncludeDependencies: false}); err != nil {
 			return nil, err
 		} else if len(dependants) > 0 {
 			for _, dep := range dependants {
-				var depPkg v1alpha1.Package
-				if err := p.Get(ctx, types.NamespacedName{Name: dep.Name}, &depPkg); err != nil {
-					return nil, err
-				} else if depPkg.DeletionTimestamp.IsZero() {
-					return nil, newConflictErrorDelete(dependants[0])
+				if dep.Pkg.DeletionTimestamp.IsZero() {
+					return nil, newConflictErrorDelete(dep.Pkg.Name)
 				}
 			}
 		}
@@ -105,9 +101,11 @@ func (p *PackageValidatingWebhook) validateCreateOrUpdate(ctx context.Context, p
 
 	if result, err := p.Validate(ctx, pkg, &manifest); err != nil {
 		return err
-	} else if len(result.Conflicts) > 0 {
+	} 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 {
 		// Conflicts are not allowed.
-		return newConflictError(result.Conflicts)
+		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.
diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go
index cc22d41..ca64593 100644
--- a/pkg/bootstrap/bootstrap.go
+++ b/pkg/bootstrap/bootstrap.go
@@ -5,7 +5,9 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/glasskube/glasskube/internal/config"
 	"github.com/glasskube/glasskube/internal/constants"
+	"github.com/glasskube/glasskube/internal/releaseinfo"
 
 	"github.com/fatih/color"
 	"github.com/glasskube/glasskube/internal/clientutils"
@@ -22,33 +24,45 @@ import (
 )
 
 type BootstrapClient struct {
-	url          string
-	version      string
 	clientConfig *rest.Config
 }
 
+type BootstrapOptions struct {
+	Type   BootstrapType
+	Url    string
+	Latest bool
+}
+
+func DefaultOptions() BootstrapOptions {
+	return BootstrapOptions{Type: BootstrapTypeAio, Latest: config.IsDevBuild()}
+}
+
 const installMessage = `
 ## Installing GLASSKUBE ##
 🧊 The missing Package Manager for Kubernetes 📦`
 
-func NewBootstrapClient(config *rest.Config, url string, version string, bootstrapType BootstrapType) *BootstrapClient {
-	if url == "" {
-		url = fmt.Sprintf("https://github.com/glasskube/glasskube/releases/download/v%v/manifest-%v.yaml",
-			version, bootstrapType)
-	}
-
-	return &BootstrapClient{
-		url:          url,
-		version:      version,
-		clientConfig: config,
-	}
+func NewBootstrapClient(config *rest.Config) *BootstrapClient {
+	return &BootstrapClient{clientConfig: config}
 }
 
-func (c *BootstrapClient) Bootstrap(ctx context.Context) error {
+func (c *BootstrapClient) Bootstrap(ctx context.Context, options BootstrapOptions) error {
 	fmt.Println(installMessage)
 
-	statusMessage("Fetching Glasskube manifest from "+c.url, true)
-	manifests, err := clientutils.FetchResources(c.url)
+	if options.Url == "" {
+		version := config.Version
+		if options.Latest {
+			if releaseInfo, err := releaseinfo.FetchLatestRelease(); err != nil {
+				return fmt.Errorf("could not determine latest version: %w", err)
+			} else {
+				version = releaseInfo.Version
+			}
+		}
+		options.Url = fmt.Sprintf("https://github.com/glasskube/glasskube/releases/download/v%v/manifest-%v.yaml",
+			version, options.Type)
+	}
+
+	statusMessage("Fetching Glasskube manifest from "+options.Url, true)
+	manifests, err := clientutils.FetchResources(options.Url)
 	if err != nil {
 		statusMessage("Couldn't fetch Glasskube manifests", false)
 		return err


Test patch:
diff --git a/internal/dependency/manager_test.go b/internal/dependency/manager_test.go
index 97a793c..a037763 100644
--- a/internal/dependency/manager_test.go
+++ b/internal/dependency/manager_test.go
@@ -2,6 +2,7 @@ package dependency
 
 import (
 	"context"
+	"strings"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
 	. "github.com/onsi/ginkgo/v2"
@@ -24,6 +25,34 @@ func (a *testClientAdapter) GetPackage(ctx context.Context, pkgName string) (*v1
 	}
 }
 
+func (a *testClientAdapter) GetPackageInfo(ctx context.Context, pkgInfoName string) (*v1alpha1.PackageInfo, error) {
+	if strings.HasPrefix(pkgInfoName, "P") && pi != nil {
+		return pi, nil
+	} else if strings.HasPrefix(pkgInfoName, "D") && di != nil {
+		return di, nil
+	} else if strings.HasPrefix(pkgInfoName, "E") && ei != nil {
+		return ei, nil
+	} else {
+		return nil, nil
+	}
+}
+
+func (a *testClientAdapter) ListPackages(ctx context.Context) (*v1alpha1.PackageList, error) {
+	var items []v1alpha1.Package
+	if p != nil {
+		items = append(items, *p)
+	}
+	if d != nil {
+		items = append(items, *d)
+	}
+	if e != nil {
+		items = append(items, *e)
+	}
+	return &v1alpha1.PackageList{
+		Items: items,
+	}, nil
+}
+
 type testRepoAdapter struct {
 }
 
diff --git a/internal/webhook/package_validator_test.go b/internal/webhook/package_validator_test.go
index f9f9960..039e25d 100644
--- a/internal/webhook/package_validator_test.go
+++ b/internal/webhook/package_validator_test.go
@@ -25,10 +25,11 @@ func newPackageValidatingWebhook(objects ...client.Object) *PackageValidatingWeb
 	fakeClient := fake.NewClientBuilder().
 		WithObjects(objects...).
 		Build()
+	ownerManager := owners.NewOwnerManager(scheme.Scheme)
 	return &PackageValidatingWebhook{
 		Client:       fakeClient,
-		OwnerManager: owners.NewOwnerManager(scheme.Scheme),
-		DependendcyManager: dependency.NewDependencyManager(ctrladapter.NewControllerRuntimeAdapter(fakeClient)).
+		OwnerManager: ownerManager,
+		DependendcyManager: dependency.NewDependencyManager(ctrladapter.NewControllerRuntimeAdapter(fakeClient), ownerManager).
 			WithRepo(&fakeRepo),
 		repo: &fakeRepo,
 	}
@@ -204,5 +205,7 @@ var _ = Describe("PackageValidatingWebhook", Ordered, func() {
 				})
 			})
 		})
+
+		// TODO tests without owner reference but with owning ref from the other side
 	})
 })