Repository: glasskube/glasskube
Pull request #455 (2024-04-02 13:22:05)

feat: add resolving, validating value configurations
<!-- Thanks for creating this pull request 🤗 Please make sure you followed the conventional commit -->

<!-- If this pull request closes an issue, please mention the issue number below -->
Closes #122

## 📑 Description
This PR includes two additions:
- A `Resolver` that is able to recursively resolve the actual values of a packages value configurations.
- A function that validates a map of already resolved values against a map of value definitions from a package manifest.

Both features are not used in real code, but I added a bunch of tests, so I think they should work correctly. This should serve as a good foundation so we can finally start with #118, #119, #120 and #121.

## ✅ Checks
- [ ] My code requires changes to the documentation
- [ ] I have updated the documentation as required

## ℹ Additional Information
Minor addition: I added a `Pattern` constraint so a package author can specify a regular expression that a value must match.


Patch:
diff --git a/api/v1alpha1/package_manifest_values.go b/api/v1alpha1/package_manifest_values.go
index 11fefb1..dca42e4 100644
--- a/api/v1alpha1/package_manifest_values.go
+++ b/api/v1alpha1/package_manifest_values.go
@@ -55,11 +55,12 @@ type ValueDefinitionMetadata struct {
 }
 
 type ValueDefinitionConstraints struct {
-	Required  bool `json:"required,omitempty"`
-	Min       int  `json:"min,omitempty"`
-	Max       int  `json:"max,omitempty"`
-	MinLength int  `json:"minLength,omitempty"`
-	MaxLength int  `json:"maxLength,omitempty"`
+	Required  bool   `json:"required,omitempty"`
+	Min       int    `json:"min,omitempty"`
+	Max       int    `json:"max,omitempty"`
+	MinLength int    `json:"minLength,omitempty"`
+	MaxLength int    `json:"maxLength,omitempty"`
+	Pattern   string `json:"pattern,omitempty"`
 }
 
 type PartialJsonPatch struct {
diff --git a/cmd/glasskube/cmd/install.go b/cmd/glasskube/cmd/install.go
index e70f996..16c1974 100644
--- a/cmd/glasskube/cmd/install.go
+++ b/cmd/glasskube/cmd/install.go
@@ -7,9 +7,9 @@ import (
 
 	"github.com/fatih/color"
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	clientadapter "github.com/glasskube/glasskube/internal/adapter/goclient"
 	"github.com/glasskube/glasskube/internal/cliutils"
 	"github.com/glasskube/glasskube/internal/dependency"
-	clientadapter "github.com/glasskube/glasskube/internal/dependency/adapter/goclient"
 	"github.com/glasskube/glasskube/internal/repo"
 	"github.com/glasskube/glasskube/pkg/client"
 	"github.com/glasskube/glasskube/pkg/condition"
@@ -35,7 +35,7 @@ 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.NewPackageClientAdapter(pkgClient))
 		installer := install.NewInstaller(pkgClient).WithStatusWriter(statuswriter.Spinner())
 		bold := color.New(color.Bold).SprintFunc()
 		packageName := args[0]
diff --git a/cmd/glasskube/cmd/uninstall.go b/cmd/glasskube/cmd/uninstall.go
index 02e35f6..db62cb3 100644
--- a/cmd/glasskube/cmd/uninstall.go
+++ b/cmd/glasskube/cmd/uninstall.go
@@ -5,9 +5,9 @@ import (
 	"os"
 
 	"github.com/fatih/color"
+	clientadapter "github.com/glasskube/glasskube/internal/adapter/goclient"
 	"github.com/glasskube/glasskube/internal/cliutils"
 	"github.com/glasskube/glasskube/internal/dependency"
-	clientadapter "github.com/glasskube/glasskube/internal/dependency/adapter/goclient"
 	pkgClient "github.com/glasskube/glasskube/pkg/client"
 	"github.com/glasskube/glasskube/pkg/statuswriter"
 	"github.com/glasskube/glasskube/pkg/uninstall"
@@ -30,7 +30,7 @@ var uninstallCmd = &cobra.Command{
 		ctx := cmd.Context()
 		currentContext := pkgClient.RawConfigFromContext(cmd.Context()).CurrentContext
 		client := pkgClient.FromContext(cmd.Context())
-		dm := dependency.NewDependencyManager(clientadapter.NewGoClientAdapter(client))
+		dm := dependency.NewDependencyManager(clientadapter.NewPackageClientAdapter(client))
 
 		if g, err := dm.NewGraph(ctx); err != nil {
 			fmt.Fprintf(os.Stderr, "❌ Error validating uninstall: %v\n", err)
diff --git a/config/crd/bases/packages.glasskube.dev_packageinfos.yaml b/config/crd/bases/packages.glasskube.dev_packageinfos.yaml
index 580cc1f..b1c8eda 100644
--- a/config/crd/bases/packages.glasskube.dev_packageinfos.yaml
+++ b/config/crd/bases/packages.glasskube.dev_packageinfos.yaml
@@ -256,6 +256,8 @@ spec:
                               type: integer
                             minLength:
                               type: integer
+                            pattern:
+                              type: string
                             required:
                               type: boolean
                           type: object
diff --git a/internal/adapter/controllerruntime/kubernetes_client_adapter.go b/internal/adapter/controllerruntime/kubernetes_client_adapter.go
new file mode 100644
index 0000000..f04fea6
--- /dev/null
+++ b/internal/adapter/controllerruntime/kubernetes_client_adapter.go
@@ -0,0 +1,43 @@
+package controllerruntime
+
+import (
+	"context"
+
+	"github.com/glasskube/glasskube/internal/adapter"
+	corev1 "k8s.io/api/core/v1"
+	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type ctrlKubernetsClientAdapter struct {
+	client ctrlclient.Client
+}
+
+// GetConfigMap implements adapter.KubernetesClientAdapter.
+func (c *ctrlKubernetsClientAdapter) GetConfigMap(ctx context.Context, name string, namespace string) (
+	*corev1.ConfigMap,
+	error,
+) {
+	var cm corev1.ConfigMap
+	if err := c.client.Get(ctx, ctrlclient.ObjectKey{Name: name, Namespace: namespace}, &cm); err != nil {
+		return nil, err
+	} else {
+		return &cm, nil
+	}
+}
+
+// GetSecret implements adapter.KubernetesClientAdapter.
+func (c *ctrlKubernetsClientAdapter) GetSecret(ctx context.Context, name string, namespace string) (
+	*corev1.Secret,
+	error,
+) {
+	var s corev1.Secret
+	if err := c.client.Get(ctx, ctrlclient.ObjectKey{Name: name, Namespace: namespace}, &s); err != nil {
+		return nil, err
+	} else {
+		return &s, nil
+	}
+}
+
+func NewKubernetesClientAdapter(client ctrlclient.Client) adapter.KubernetesClientAdapter {
+	return &ctrlKubernetsClientAdapter{client: client}
+}
diff --git a/internal/adapter/controllerruntime/package_client_adapter.go b/internal/adapter/controllerruntime/package_client_adapter.go
new file mode 100644
index 0000000..31755f5
--- /dev/null
+++ b/internal/adapter/controllerruntime/package_client_adapter.go
@@ -0,0 +1,46 @@
+package controllerruntime
+
+import (
+	"context"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/adapter"
+	"k8s.io/apimachinery/pkg/types"
+	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type ControllerRuntimeAdapter struct {
+	client ctrlclient.Client
+}
+
+func NewPackageClientAdapter(client ctrlclient.Client) adapter.PackageClientAdapter {
+	return &ControllerRuntimeAdapter{client: client}
+}
+
+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, &ctrlclient.ListOptions{}); err != nil {
+		return nil, err
+	}
+	return &pkgList, nil
+}
+
+// GetPackage implements adapter.PackageClientAdapter.
+func (a *ControllerRuntimeAdapter) GetPackage(ctx context.Context, name string) (*v1alpha1.Package, error) {
+	var pkg v1alpha1.Package
+	return &pkg, a.client.Get(ctx, ctrlclient.ObjectKey{Name: name}, &pkg)
+}
diff --git a/internal/adapter/goclient/kubernetes_client_adapter.go b/internal/adapter/goclient/kubernetes_client_adapter.go
new file mode 100644
index 0000000..b3112ae
--- /dev/null
+++ b/internal/adapter/goclient/kubernetes_client_adapter.go
@@ -0,0 +1,34 @@
+package goclient
+
+import (
+	"context"
+
+	"github.com/glasskube/glasskube/internal/adapter"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+)
+
+type clientSetKubernetesClientAdapter struct {
+	clientset kubernetes.Clientset
+}
+
+// GetConfigMap implements adapter.KubernetesClientAdapter.
+func (c *clientSetKubernetesClientAdapter) GetConfigMap(ctx context.Context, name string, namespace string) (
+	*corev1.ConfigMap,
+	error,
+) {
+	return c.clientset.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})
+}
+
+// GetSecret implements adapter.KubernetesClientAdapter.
+func (c *clientSetKubernetesClientAdapter) GetSecret(ctx context.Context, name string, namespace string) (
+	*corev1.Secret,
+	error,
+) {
+	return c.clientset.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
+}
+
+func NewKubernetesClientAdapter(clientset kubernetes.Clientset) adapter.KubernetesClientAdapter {
+	return &clientSetKubernetesClientAdapter{clientset: clientset}
+}
diff --git a/internal/adapter/goclient/package_client_adapter.go b/internal/adapter/goclient/package_client_adapter.go
new file mode 100644
index 0000000..8fed75f
--- /dev/null
+++ b/internal/adapter/goclient/package_client_adapter.go
@@ -0,0 +1,43 @@
+package goclient
+
+import (
+	"context"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/adapter"
+	"github.com/glasskube/glasskube/pkg/client"
+)
+
+type goClientPackageAdapter struct {
+	pkgClient client.PackageV1Alpha1Client
+}
+
+func NewPackageClientAdapter(pkgClient client.PackageV1Alpha1Client) adapter.PackageClientAdapter {
+	return &goClientPackageAdapter{pkgClient: pkgClient}
+}
+
+func (a *goClientPackageAdapter) 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 *goClientPackageAdapter) 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
+}
+
+// GetPackage implements adapter.PackageClientAdapter.
+func (a *goClientPackageAdapter) GetPackage(ctx context.Context, name string) (*v1alpha1.Package, error) {
+	var pkg v1alpha1.Package
+	return &pkg, a.pkgClient.Packages().Get(ctx, name, &pkg)
+}
diff --git a/internal/dependency/adapter/adapter.go b/internal/adapter/types.go
similarity index 55%
rename from internal/dependency/adapter/adapter.go
rename to internal/adapter/types.go
index 2d7eb5e..3f1aa44 100644
--- a/internal/dependency/adapter/adapter.go
+++ b/internal/adapter/types.go
@@ -4,11 +4,18 @@ import (
 	"context"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	v1 "k8s.io/api/core/v1"
 )
 
-type ClientAdapter interface {
+type PackageClientAdapter interface {
 	GetPackageInfo(ctx context.Context, pkgInfoName string) (*v1alpha1.PackageInfo, error)
 	ListPackages(ctx context.Context) (*v1alpha1.PackageList, error)
+	GetPackage(ctx context.Context, name string) (*v1alpha1.Package, error)
+}
+
+type KubernetesClientAdapter interface {
+	GetSecret(ctx context.Context, name, namespace string) (*v1.Secret, error)
+	GetConfigMap(ctx context.Context, name, namespace string) (*v1.ConfigMap, error)
 }
 
 type RepoAdapter interface {
diff --git a/internal/controller/package_controller.go b/internal/controller/package_controller.go
index 903076c..724cb3e 100644
--- a/internal/controller/package_controller.go
+++ b/internal/controller/package_controller.go
@@ -21,7 +21,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/glasskube/glasskube/internal/dependency/adapter/controllerruntime"
+	ctrladapter "github.com/glasskube/glasskube/internal/adapter/controllerruntime"
 	"github.com/glasskube/glasskube/internal/names"
 	"k8s.io/apimachinery/pkg/types"
 
@@ -105,7 +105,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(ctrladapter.NewPackageClientAdapter(r.Client))
 	}
 	controllerBuilder := ctrl.NewControllerManagedBy(mgr).For(&packagesv1alpha1.Package{})
 	for _, adapter := range []manifest.ManifestAdapter{r.HelmAdapter, r.KustomizeAdapter, r.ManifestAdapter} {
diff --git a/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go b/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go
deleted file mode 100644
index 9e68ba2..0000000
--- a/internal/dependency/adapter/controllerruntime/controllerruntimeadapter.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package controllerruntime
-
-import (
-	"context"
-
-	"github.com/glasskube/glasskube/api/v1alpha1"
-	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-)
-
-type ControllerRuntimeAdapter struct {
-	client client.Client
-}
-
-func NewControllerRuntimeAdapter(client client.Client) *ControllerRuntimeAdapter {
-	return &ControllerRuntimeAdapter{
-		client: client,
-	}
-}
-
-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
deleted file mode 100644
index 4cad211..0000000
--- a/internal/dependency/adapter/goclient/goclientadapter.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package goclient
-
-import (
-	"context"
-
-	"github.com/glasskube/glasskube/api/v1alpha1"
-	"github.com/glasskube/glasskube/pkg/client"
-)
-
-type GoClientAdapter struct {
-	pkgClient client.PackageV1Alpha1Client
-}
-
-func NewGoClientAdapter(pkgClient client.PackageV1Alpha1Client) *GoClientAdapter {
-	return &GoClientAdapter{
-		pkgClient: pkgClient,
-	}
-}
-
-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 f5b6119..010501b 100644
--- a/internal/dependency/manager.go
+++ b/internal/dependency/manager.go
@@ -9,7 +9,7 @@ import (
 	"github.com/glasskube/glasskube/internal/names"
 	"go.uber.org/multierr"
 
-	"github.com/glasskube/glasskube/internal/dependency/adapter"
+	"github.com/glasskube/glasskube/internal/adapter"
 	"github.com/glasskube/glasskube/internal/dependency/graph"
 
 	"github.com/Masterminds/semver/v3"
@@ -19,11 +19,11 @@ import (
 )
 
 type DependendcyManager struct {
-	clientAdapter adapter.ClientAdapter
+	clientAdapter adapter.PackageClientAdapter
 	repoAdapter   adapter.RepoAdapter
 }
 
-func NewDependencyManager(adapter adapter.ClientAdapter) *DependendcyManager {
+func NewDependencyManager(adapter adapter.PackageClientAdapter) *DependendcyManager {
 	return &DependendcyManager{
 		clientAdapter: adapter,
 		repoAdapter:   &defaultRepoAdapter{repo: repo.DefaultClient},
diff --git a/internal/manifestvalues/errors.go b/internal/manifestvalues/errors.go
new file mode 100644
index 0000000..d988dc7
--- /dev/null
+++ b/internal/manifestvalues/errors.go
@@ -0,0 +1,74 @@
+package manifestvalues
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+)
+
+type KeyError struct {
+	key string
+}
+
+func (err *KeyError) Error() string {
+	return fmt.Sprintf("no such key: %v", err.key)
+}
+
+func NewKeyError(key string) error {
+	return &KeyError{key: key}
+}
+
+type ValueTypeError struct {
+	t v1alpha1.ValueType
+}
+
+func (err *ValueTypeError) Error() string {
+	return fmt.Sprintf("unhandled type: %v (this is a bug)", err.t)
+}
+
+func NewValueTypeError(t v1alpha1.ValueType) error {
+	return &ValueTypeError{t: t}
+}
+
+func NewValidationError(name string, cause error) error {
+	if cause == nil {
+		return nil
+	}
+	return fmt.Errorf("validation error for value %v: %w", name, cause)
+}
+
+func NewConfigMapRefError(source v1alpha1.ObjectKeyValueSource, cause error) error {
+	return fmt.Errorf("cannot resolve reference to ConfigMap %v.%v: %w", source.Name, source.Namespace, cause)
+}
+
+func NewSecretRefError(source v1alpha1.ObjectKeyValueSource, cause error) error {
+	return fmt.Errorf("cannot resolve reference to Secret %v.%v: %w", source.Name, source.Namespace, cause)
+}
+
+func NewPackageRefError(source v1alpha1.PackageValueSource, cause error) error {
+	return fmt.Errorf("cannot resolve reference to value %v in Package %v: %w", source.Value, source.Name, cause)
+}
+
+func NewOptionsError(options []string) error {
+	return fmt.Errorf("value must be one of: %v", strings.Join(options, ", "))
+}
+
+func NewFormatError(format string, cause error) error {
+	return fmt.Errorf("value must be a %v: %w", format, cause)
+}
+
+func NewPatternError(pattern string) error {
+	return fmt.Errorf("value must match '%v'", pattern)
+}
+
+var (
+	ErrNoDef               = errors.New("no value definition found")
+	ErrConstraint          = errors.New("constraint violation")
+	ErrConstraintRequired  = fmt.Errorf("%w: Required", ErrConstraint)
+	ErrConstraintMin       = fmt.Errorf("%w: Min", ErrConstraint)
+	ErrConstraintMax       = fmt.Errorf("%w: Max", ErrConstraint)
+	ErrConstraintMinLength = fmt.Errorf("%w: MinLength", ErrConstraint)
+	ErrConstraintMaxLength = fmt.Errorf("%w: MaxLength", ErrConstraint)
+)
diff --git a/internal/manifestvalues/resolver.go b/internal/manifestvalues/resolver.go
new file mode 100644
index 0000000..5a624e3
--- /dev/null
+++ b/internal/manifestvalues/resolver.go
@@ -0,0 +1,95 @@
+package manifestvalues
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"fmt"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/adapter"
+)
+
+type resolver struct {
+	pkg    adapter.PackageClientAdapter
+	client adapter.KubernetesClientAdapter
+}
+
+func NewResolver(pkg adapter.PackageClientAdapter, client adapter.KubernetesClientAdapter) *resolver {
+	return &resolver{pkg: pkg, client: client}
+}
+
+func (r *resolver) Resolve(ctx context.Context, values map[string]v1alpha1.ValueConfiguration) (
+	map[string]string,
+	error,
+) {
+	resolvedValues := make(map[string]string)
+	for name, value := range values {
+		if resolved, err := r.ResolveValue(ctx, value); err != nil {
+			return nil, fmt.Errorf("cannot resolve value %v: %w", name, err)
+		} else {
+			resolvedValues[name] = resolved
+		}
+	}
+	return resolvedValues, nil
+}
+
+func (r *resolver) ResolveValue(ctx context.Context, value v1alpha1.ValueConfiguration) (string, error) {
+	if value.Value != nil {
+		return *value.Value, nil
+	} else if value.ValueFrom != nil {
+		if r, err := r.resolveReference(ctx, *value.ValueFrom); err != nil {
+			return "", err
+		} else {
+			return r, nil
+		}
+	} else {
+		return "", errors.New("cannot resolve empty value")
+	}
+}
+
+func (r *resolver) resolveReference(ctx context.Context, ref v1alpha1.ValueReference) (string, error) {
+	if ref.ConfigMapRef != nil {
+		return r.resolveConfigMapRef(ctx, *ref.ConfigMapRef)
+	} else if ref.SecretRef != nil {
+		return r.resolveSecretRef(ctx, *ref.SecretRef)
+	} else if ref.PackageRef != nil {
+		return r.resolvePackageRef(ctx, *ref.PackageRef)
+	} else {
+		return "", errors.New("cannot resolve empty reference")
+	}
+}
+
+func (r *resolver) resolveConfigMapRef(ctx context.Context, ref v1alpha1.ObjectKeyValueSource) (string, error) {
+	if c, err := r.client.GetConfigMap(ctx, ref.Name, ref.Namespace); err != nil {
+		return "", NewConfigMapRefError(ref, err)
+	} else if v, ok := c.Data[ref.Key]; !ok {
+		return "", NewConfigMapRefError(ref, NewKeyError(ref.Key))
+	} else {
+		return v, nil
+	}
+}
+
+func (r *resolver) resolveSecretRef(ctx context.Context, ref v1alpha1.ObjectKeyValueSource) (string, error) {
+	if c, err := r.client.GetSecret(ctx, ref.Name, ref.Namespace); err != nil {
+		return "", NewSecretRefError(ref, err)
+	} else if v, ok := c.Data[ref.Key]; !ok {
+		return "", NewSecretRefError(ref, NewKeyError(ref.Key))
+	} else if decoded, err := base64.StdEncoding.DecodeString(string(v)); err != nil {
+		return "", NewSecretRefError(ref, err)
+	} else {
+		return string(decoded), nil
+	}
+}
+
+func (r *resolver) resolvePackageRef(ctx context.Context, ref v1alpha1.PackageValueSource) (string, error) {
+	if pkg, err := r.pkg.GetPackage(ctx, ref.Name); err != nil {
+		return "", NewPackageRefError(ref, err)
+	} else if value, ok := pkg.Spec.Values[ref.Value]; !ok {
+		return "", NewPackageRefError(ref, NewKeyError(ref.Value))
+	} else if resolved, err := r.ResolveValue(ctx, value); err != nil {
+		return "", NewPackageRefError(ref, err)
+	} else {
+		return resolved, nil
+	}
+}
diff --git a/internal/manifestvalues/validate.go b/internal/manifestvalues/validate.go
new file mode 100644
index 0000000..a9539d0
--- /dev/null
+++ b/internal/manifestvalues/validate.go
@@ -0,0 +1,120 @@
+package manifestvalues
+
+import (
+	"regexp"
+	"strconv"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"go.uber.org/multierr"
+)
+
+type validateFn func(def v1alpha1.ValueDefinition, value string) error
+
+var (
+	validateMinLength validateFn = func(def v1alpha1.ValueDefinition, value string) error {
+		if def.Constraints.MinLength > 0 && len(value) < def.Constraints.MinLength {
+			return ErrConstraintMinLength
+		}
+		return nil
+	}
+
+	validateMaxLength validateFn = func(def v1alpha1.ValueDefinition, value string) error {
+		if def.Constraints.MaxLength > 0 && len(value) > def.Constraints.MaxLength {
+			return ErrConstraintMaxLength
+		}
+		return nil
+	}
+
+	validateFormatNumber validateFn = func(def v1alpha1.ValueDefinition, value string) error {
+		if _, err := strconv.Atoi(value); err != nil {
+			return NewFormatError("number", err)
+		}
+		return nil
+	}
+
+	validateFormatBoolean validateFn = func(def v1alpha1.ValueDefinition, value string) error {
+		if _, err := strconv.ParseBool(value); err != nil {
+			return NewFormatError("boolean", err)
+		}
+		return nil
+	}
+
+	validateMin validateFn = func(def v1alpha1.ValueDefinition, value string) error {
+		if def.Constraints.Min > 0 {
+			if i, err := strconv.Atoi(value); err != nil {
+				return err
+			} else if i < def.Constraints.Min {
+				return ErrConstraintMin
+			}
+		}
+		return nil
+	}
+
+	validateMax validateFn = func(def v1alpha1.ValueDefinition, value string) error {
+		if def.Constraints.Max > 0 {
+			if i, err := strconv.Atoi(value); err != nil {
+				return err
+			} else if i > def.Constraints.Max {
+				return ErrConstraintMax
+			}
+		}
+		return nil
+	}
+
+	validateOptions validateFn = func(def v1alpha1.ValueDefinition, value string) error {
+		for _, o := range def.Options {
+			if value == o {
+				return nil
+			}
+		}
+		return NewOptionsError(def.Options)
+	}
+
+	validatePattern validateFn = func(def v1alpha1.ValueDefinition, value string) error {
+		if len(def.Constraints.Pattern) > 0 {
+			if re, err := regexp.Compile(def.Constraints.Pattern); err != nil {
+				return err
+			} else if !re.MatchString(value) {
+				return NewPatternError(def.Constraints.Pattern)
+			}
+		}
+		return nil
+	}
+)
+
+type validateFns []validateFn
+
+func (v *validateFns) Validate(def v1alpha1.ValueDefinition, value string) (err error) {
+	for _, fn := range *v {
+		multierr.AppendInto(&err, fn(def, value))
+	}
+	return
+}
+
+var validatorsForType = map[v1alpha1.ValueType]validateFns{
+	v1alpha1.ValueTypeText:    {validateMaxLength, validateMinLength, validatePattern},
+	v1alpha1.ValueTypeNumber:  {validateFormatNumber, validateMin, validateMax, validatePattern},
+	v1alpha1.ValueTypeOptions: {validateOptions},
+	v1alpha1.ValueTypeBoolean: {validateFormatBoolean},
+}
+
+func ValidateResolvedValues(manifest v1alpha1.PackageManifest, values map[string]string) (err error) {
+	namesFromDef := make(map[string]struct{})
+	for name, def := range manifest.ValueDefinitions {
+		namesFromDef[name] = struct{}{}
+		if validators, ok := validatorsForType[def.Type]; !ok {
+			multierr.AppendInto(&err, NewValidationError(name, NewValueTypeError(def.Type)))
+		} else if value, ok := values[name]; ok {
+			multierr.AppendInto(&err, NewValidationError(name, validators.Validate(def, value)))
+		} else if def.Constraints.Required {
+			multierr.AppendInto(&err, NewValidationError(name, ErrConstraintRequired))
+		}
+	}
+	for name := range values {
+		// check for value configurations that don't have a matching value definition
+		if _, ok := namesFromDef[name]; !ok {
+			multierr.AppendInto(&err, NewValidationError(name, ErrNoDef))
+		}
+	}
+	return
+}
diff --git a/internal/web/server.go b/internal/web/server.go
index cda2b02..c9141cf 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -17,9 +17,9 @@ import (
 	"sync"
 	"syscall"
 
+	clientadapter "github.com/glasskube/glasskube/internal/adapter/goclient"
 	"github.com/glasskube/glasskube/internal/config"
 	"github.com/glasskube/glasskube/internal/dependency"
-	"github.com/glasskube/glasskube/internal/dependency/adapter/goclient"
 
 	"github.com/glasskube/glasskube/pkg/update"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -587,7 +587,7 @@ func (server *server) initKubeConfig() ServerConfigError {
 	server.restConfig = restConfig
 	server.rawConfig = rawConfig
 	server.pkgClient = client
-	server.dependencyMgr = dependency.NewDependencyManager(goclient.NewGoClientAdapter(server.pkgClient))
+	server.dependencyMgr = dependency.NewDependencyManager(clientadapter.NewPackageClientAdapter(server.pkgClient))
 	return nil
 }
 
diff --git a/internal/webhook/package_validator.go b/internal/webhook/package_validator.go
index fd8f2ef..ffba3e2 100644
--- a/internal/webhook/package_validator.go
+++ b/internal/webhook/package_validator.go
@@ -5,9 +5,9 @@ import (
 	"errors"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	ctrladapter "github.com/glasskube/glasskube/internal/adapter/controllerruntime"
 	"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"
@@ -32,7 +32,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.NewPackageClientAdapter(p.Client))
 	p.repo = repo.DefaultClient
 	return ctrl.NewWebhookManagedBy(mgr).
 		For(&v1alpha1.Package{}).
diff --git a/pkg/update/update.go b/pkg/update/update.go
index ff2d5f7..8bc249a 100644
--- a/pkg/update/update.go
+++ b/pkg/update/update.go
@@ -6,8 +6,8 @@ import (
 	"fmt"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	clientadapter "github.com/glasskube/glasskube/internal/adapter/goclient"
 	"github.com/glasskube/glasskube/internal/dependency"
-	clientadapter "github.com/glasskube/glasskube/internal/dependency/adapter/goclient"
 	"github.com/glasskube/glasskube/internal/repo"
 	"github.com/glasskube/glasskube/internal/semver"
 	"github.com/glasskube/glasskube/pkg/client"
@@ -56,7 +56,7 @@ func NewUpdater(client client.PackageV1Alpha1Client) *updater {
 	return &updater{
 		client: client,
 		status: statuswriter.Noop(),
-		dm:     dependency.NewDependencyManager(clientadapter.NewGoClientAdapter(client)),
+		dm:     dependency.NewDependencyManager(clientadapter.NewPackageClientAdapter(client)),
 	}
 }
 
diff --git a/website/static/schemas/v1/package-manifest.json b/website/static/schemas/v1/package-manifest.json
index 0677613..9c9567d 100644
--- a/website/static/schemas/v1/package-manifest.json
+++ b/website/static/schemas/v1/package-manifest.json
@@ -194,6 +194,9 @@
         },
         "maxLength": {
           "type": "integer"
+        },
+        "pattern": {
+          "type": "string"
         }
       },
       "additionalProperties": false,


Test patch:
diff --git a/internal/dependency/manager_test.go b/internal/dependency/manager_test.go
index f989cb2..a2658c0 100644
--- a/internal/dependency/manager_test.go
+++ b/internal/dependency/manager_test.go
@@ -44,6 +44,11 @@ func (a *testClientAdapter) ListPackages(ctx context.Context) (*v1alpha1.Package
 	}, nil
 }
 
+// GetPackage implements adapter.PackageClientAdapter.
+func (a *testClientAdapter) GetPackage(ctx context.Context, name string) (*v1alpha1.Package, error) {
+	panic("unimplemented")
+}
+
 func createDependencyManager() *DependendcyManager {
 	return NewDependencyManager(&testClientAdapter{}).WithRepo(fakeRepo)
 }
diff --git a/internal/manifestvalues/manifestvalues_suite_test.go b/internal/manifestvalues/manifestvalues_suite_test.go
new file mode 100644
index 0000000..cc06f5f
--- /dev/null
+++ b/internal/manifestvalues/manifestvalues_suite_test.go
@@ -0,0 +1,13 @@
+package manifestvalues
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestDependency(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "ManifestValues Suite")
+}
diff --git a/internal/manifestvalues/resolver_test.go b/internal/manifestvalues/resolver_test.go
new file mode 100644
index 0000000..6a2bcd1
--- /dev/null
+++ b/internal/manifestvalues/resolver_test.go
@@ -0,0 +1,105 @@
+package manifestvalues
+
+import (
+	"context"
+	"encoding/base64"
+
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	"github.com/glasskube/glasskube/internal/adapter/controllerruntime"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	clientscheme "k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+func newTestResolver(initObjs ...runtime.Object) *resolver {
+	scheme := runtime.NewScheme()
+	Expect(clientscheme.AddToScheme(scheme)).NotTo(HaveOccurred())
+	Expect(v1alpha1.AddToScheme(scheme)).NotTo(HaveOccurred())
+	client := fake.NewClientBuilder().
+		WithScheme(scheme).
+		WithRuntimeObjects(initObjs...).
+		Build()
+	return NewResolver(
+		controllerruntime.NewPackageClientAdapter(client),
+		controllerruntime.NewKubernetesClientAdapter(client),
+	)
+}
+
+var _ = Describe("resolver", func() {
+	var testConst = "test"
+
+	It("should resolve literal value", func(ctx context.Context) {
+		resolver := newTestResolver()
+		result, err := resolver.Resolve(ctx, map[string]v1alpha1.ValueConfiguration{
+			"test": {Value: &testConst},
+		})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(result).To(Equal(map[string]string{"test": "test"}))
+	})
+
+	It("should resolve ConfigMap reference value", func(ctx context.Context) {
+		resolver := newTestResolver(&corev1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "test"},
+			Data:       map[string]string{"test": "test"},
+		})
+		result, err := resolver.Resolve(ctx, map[string]v1alpha1.ValueConfiguration{
+			"test": {
+				ValueFrom: &v1alpha1.ValueReference{
+					ConfigMapRef: &v1alpha1.ObjectKeyValueSource{Name: "test", Namespace: "test", Key: "test"},
+				},
+			},
+		})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(result).To(Equal(map[string]string{"test": "test"}))
+	})
+
+	It("should resolve Secret reference value", func(ctx context.Context) {
+		resolver := newTestResolver(&corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "test"},
+			Data:       map[string][]byte{"test": []byte(base64.StdEncoding.EncodeToString([]byte("test")))},
+		})
+		result, err := resolver.Resolve(ctx, map[string]v1alpha1.ValueConfiguration{
+			"test": {
+				ValueFrom: &v1alpha1.ValueReference{
+					SecretRef: &v1alpha1.ObjectKeyValueSource{Name: "test", Namespace: "test", Key: "test"},
+				},
+			},
+		})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(result).To(Equal(map[string]string{"test": "test"}))
+	})
+
+	It("should resolve Package reference value", func(ctx context.Context) {
+		resolver := newTestResolver(
+			&v1alpha1.Package{
+				ObjectMeta: metav1.ObjectMeta{Name: "test"},
+				Spec: v1alpha1.PackageSpec{
+					Values: map[string]v1alpha1.ValueConfiguration{
+						"test": {
+							ValueFrom: &v1alpha1.ValueReference{
+								SecretRef: &v1alpha1.ObjectKeyValueSource{Name: "test", Namespace: "test", Key: "test"},
+							},
+						},
+					},
+				},
+			},
+			&corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "test"},
+				Data:       map[string][]byte{"test": []byte(base64.StdEncoding.EncodeToString([]byte("test")))},
+			},
+		)
+		result, err := resolver.Resolve(ctx, map[string]v1alpha1.ValueConfiguration{
+			"test": {
+				ValueFrom: &v1alpha1.ValueReference{
+					PackageRef: &v1alpha1.PackageValueSource{Name: "test", Value: "test"},
+				},
+			},
+		})
+		Expect(err).NotTo(HaveOccurred())
+		Expect(result).To(Equal(map[string]string{"test": "test"}))
+	})
+})
diff --git a/internal/manifestvalues/validate_test.go b/internal/manifestvalues/validate_test.go
new file mode 100644
index 0000000..54b4f98
--- /dev/null
+++ b/internal/manifestvalues/validate_test.go
@@ -0,0 +1,78 @@
+package manifestvalues
+
+import (
+	"github.com/glasskube/glasskube/api/v1alpha1"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("ValidateResolvedValues", func() {
+	emptyManifest := v1alpha1.PackageManifest{}
+	manifestWithSimpleValue := v1alpha1.PackageManifest{
+		ValueDefinitions: map[string]v1alpha1.ValueDefinition{
+			"test": {Type: v1alpha1.ValueTypeText}},
+	}
+	manifestWithRequiredValue := v1alpha1.PackageManifest{
+		ValueDefinitions: map[string]v1alpha1.ValueDefinition{
+			"test": {Type: v1alpha1.ValueTypeText, Constraints: v1alpha1.ValueDefinitionConstraints{Required: true}}},
+	}
+	manifestWithConstraints := v1alpha1.PackageManifest{
+		ValueDefinitions: map[string]v1alpha1.ValueDefinition{
+			"minmaxstr": {
+				Type: v1alpha1.ValueTypeText,
+				Constraints: v1alpha1.ValueDefinitionConstraints{
+					MinLength: 5,
+					MaxLength: 10,
+				},
+			},
+			"minmax": {
+				Type: v1alpha1.ValueTypeNumber,
+				Constraints: v1alpha1.ValueDefinitionConstraints{
+					Min: 5,
+					Max: 10,
+				},
+			},
+			"pattern": {
+				Type: v1alpha1.ValueTypeText,
+				Constraints: v1alpha1.ValueDefinitionConstraints{
+					Pattern: "a{2,3}b+",
+				},
+			},
+			"options": {
+				Type:    v1alpha1.ValueTypeOptions,
+				Options: []string{"foo", "bar"},
+			},
+			"bool": {Type: v1alpha1.ValueTypeBoolean},
+		},
+	}
+	DescribeTable("Validating values",
+		func(manifest v1alpha1.PackageManifest, values map[string]string, valid bool) {
+			err := ValidateResolvedValues(manifest, values)
+			if valid {
+				Expect(err).NotTo(HaveOccurred())
+			} else {
+				Expect(err).To(HaveOccurred())
+			}
+		},
+		Entry("When def is empty and config is empty", emptyManifest, map[string]string{}, true),
+		Entry("When value without def", emptyManifest, map[string]string{"test": "test"}, false),
+		Entry("When value with matching def", manifestWithSimpleValue, map[string]string{"test": "test"}, true),
+		Entry("When required value missing", manifestWithRequiredValue, map[string]string{}, false),
+		Entry("When required value present", manifestWithRequiredValue, map[string]string{"test": "test"}, true),
+		Entry("When MinLength violated", manifestWithConstraints, map[string]string{"minmaxstr": "aaa"}, false),
+		Entry("When MaxLength violated", manifestWithConstraints, map[string]string{"minmaxstr": "aaaaaaaaaaa"}, false),
+		Entry("When MinLength, MaxLength not violated",
+			manifestWithConstraints, map[string]string{"minmaxstr": "aaaaaaaaaa"}, true),
+		Entry("When Min violated", manifestWithConstraints, map[string]string{"minmax": "1"}, false),
+		Entry("When Max violated", manifestWithConstraints, map[string]string{"minmax": "11"}, false),
+		Entry("When Min, Max not violated", manifestWithConstraints, map[string]string{"minmax": "7"}, true),
+		Entry("When Pattern violated", manifestWithConstraints, map[string]string{"pattern": "ab"}, false),
+		Entry("When Pattern not violated", manifestWithConstraints, map[string]string{"pattern": "aaab"}, true),
+		Entry("When value not in Options", manifestWithConstraints, map[string]string{"options": "test"}, false),
+		Entry("When value in Options", manifestWithConstraints, map[string]string{"options": "foo"}, true),
+		Entry("When wrong bool format", manifestWithConstraints, map[string]string{"bool": "test"}, false),
+		Entry("When correct bool format: true", manifestWithConstraints, map[string]string{"bool": "true"}, true),
+		Entry("When correct bool format: false", manifestWithConstraints, map[string]string{"bool": "false"}, true),
+		Entry("When correct bool format: 1", manifestWithConstraints, map[string]string{"bool": "1"}, true),
+	)
+})
diff --git a/internal/webhook/package_validator_test.go b/internal/webhook/package_validator_test.go
index e5e8190..db666ef 100644
--- a/internal/webhook/package_validator_test.go
+++ b/internal/webhook/package_validator_test.go
@@ -5,9 +5,9 @@ import (
 	"time"
 
 	"github.com/glasskube/glasskube/api/v1alpha1"
+	ctrladapter "github.com/glasskube/glasskube/internal/adapter/controllerruntime"
 	"github.com/glasskube/glasskube/internal/controller/owners"
 	"github.com/glasskube/glasskube/internal/dependency"
-	ctrladapter "github.com/glasskube/glasskube/internal/dependency/adapter/controllerruntime"
 	fakerepo "github.com/glasskube/glasskube/internal/repo/client/fake"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
@@ -29,7 +29,7 @@ func newPackageValidatingWebhook(objects ...client.Object) *PackageValidatingWeb
 	return &PackageValidatingWebhook{
 		Client:       fakeClient,
 		OwnerManager: ownerManager,
-		DependendcyManager: dependency.NewDependencyManager(ctrladapter.NewControllerRuntimeAdapter(fakeClient)).
+		DependendcyManager: dependency.NewDependencyManager(ctrladapter.NewPackageClientAdapter(fakeClient)).
 			WithRepo(&fakeRepo),
 		repo: &fakeRepo,
 	}