Repository: rancher/fleet
Pull request #471 (2021-07-23 17:24:29)

Add values from
https://github.com/rancher/fleet/issues/384


Patch:
diff --git a/charts/fleet-crd/templates/crds.yaml b/charts/fleet-crd/templates/crds.yaml
index 871c5640..cf3c3d56 100644
--- a/charts/fleet-crd/templates/crds.yaml
+++ b/charts/fleet-crd/templates/crds.yaml
@@ -111,6 +111,38 @@ spec:
                       type: string
                     nullable: true
                     type: array
+                  valuesFrom:
+                    items:
+                      properties:
+                        configMapKeyRef:
+                          nullable: true
+                          properties:
+                            key:
+                              nullable: true
+                              type: string
+                            name:
+                              nullable: true
+                              type: string
+                            namespace:
+                              nullable: true
+                              type: string
+                          type: object
+                        secretKeyRef:
+                          nullable: true
+                          properties:
+                            key:
+                              nullable: true
+                              type: string
+                            name:
+                              nullable: true
+                              type: string
+                            namespace:
+                              nullable: true
+                              type: string
+                          type: object
+                      type: object
+                    nullable: true
+                    type: array
                   version:
                     nullable: true
                     type: string
@@ -446,6 +478,38 @@ spec:
                             type: string
                           nullable: true
                           type: array
+                        valuesFrom:
+                          items:
+                            properties:
+                              configMapKeyRef:
+                                nullable: true
+                                properties:
+                                  key:
+                                    nullable: true
+                                    type: string
+                                  name:
+                                    nullable: true
+                                    type: string
+                                  namespace:
+                                    nullable: true
+                                    type: string
+                                type: object
+                              secretKeyRef:
+                                nullable: true
+                                properties:
+                                  key:
+                                    nullable: true
+                                    type: string
+                                  name:
+                                    nullable: true
+                                    type: string
+                                  namespace:
+                                    nullable: true
+                                    type: string
+                                type: object
+                            type: object
+                          nullable: true
+                          type: array
                         version:
                           nullable: true
                           type: string
@@ -893,6 +957,38 @@ spec:
                           type: string
                         nullable: true
                         type: array
+                      valuesFrom:
+                        items:
+                          properties:
+                            configMapKeyRef:
+                              nullable: true
+                              properties:
+                                key:
+                                  nullable: true
+                                  type: string
+                                name:
+                                  nullable: true
+                                  type: string
+                                namespace:
+                                  nullable: true
+                                  type: string
+                              type: object
+                            secretKeyRef:
+                              nullable: true
+                              properties:
+                                key:
+                                  nullable: true
+                                  type: string
+                                name:
+                                  nullable: true
+                                  type: string
+                                namespace:
+                                  nullable: true
+                                  type: string
+                              type: object
+                          type: object
+                        nullable: true
+                        type: array
                       version:
                         nullable: true
                         type: string
@@ -1004,6 +1100,38 @@ spec:
                           type: string
                         nullable: true
                         type: array
+                      valuesFrom:
+                        items:
+                          properties:
+                            configMapKeyRef:
+                              nullable: true
+                              properties:
+                                key:
+                                  nullable: true
+                                  type: string
+                                name:
+                                  nullable: true
+                                  type: string
+                                namespace:
+                                  nullable: true
+                                  type: string
+                              type: object
+                            secretKeyRef:
+                              nullable: true
+                              properties:
+                                key:
+                                  nullable: true
+                                  type: string
+                                name:
+                                  nullable: true
+                                  type: string
+                                namespace:
+                                  nullable: true
+                                  type: string
+                              type: object
+                          type: object
+                        nullable: true
+                        type: array
                       version:
                         nullable: true
                         type: string
diff --git a/go.mod b/go.mod
index 217e272d..026087a2 100644
--- a/go.mod
+++ b/go.mod
@@ -44,11 +44,12 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/rancher/fleet/pkg/apis v0.0.0
 	github.com/rancher/gitjob v0.1.5
-	github.com/rancher/lasso v0.0.0-20210616224652-fc3ebd901c08
-	github.com/rancher/wrangler v0.8.4-0.20210721232424-82b1bc4f2de5
+	github.com/rancher/lasso v0.0.0-20210408231703-9ddd9378d08d
+	github.com/rancher/wrangler v0.8.1-0.20210521213200-39dd8bf93e9f
 	github.com/rancher/wrangler-cli v0.0.0-20200815040857-81c48cf8ab43
 	github.com/sirupsen/logrus v1.6.0
 	github.com/spf13/cobra v1.1.1
+	github.com/stretchr/testify v1.6.1
 	go.mozilla.org/sops/v3 v3.6.1
 	golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
 	golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3 // indirect
diff --git a/go.sum b/go.sum
index 215f674c..593df7e6 100644
--- a/go.sum
+++ b/go.sum
@@ -1176,13 +1176,13 @@ github.com/rancher/helm/v3 v3.3.3-fleet1 h1:5kutcoifVpkL9H6JMZVBhM1YvgSf8zTL4UQk
 github.com/rancher/helm/v3 v3.3.3-fleet1/go.mod h1:qUawjWz9THVCBVsNPJ/Q20VNEyrK7kN6lYf5loyp194=
 github.com/rancher/lasso v0.0.0-20200820172840-0e4cc0ef5cb0/go.mod h1:OhBBBO1pBwYp0hacWdnvSGOj+XE9yMLOLnaypIlic18=
 github.com/rancher/lasso v0.0.0-20200905045615-7fcb07d6a20b/go.mod h1:OhBBBO1pBwYp0hacWdnvSGOj+XE9yMLOLnaypIlic18=
-github.com/rancher/lasso v0.0.0-20210616224652-fc3ebd901c08 h1:NxR8Fh0eE7/5/5Zvlog9B5NVjWKqBSb1WYMUF7/IE5c=
-github.com/rancher/lasso v0.0.0-20210616224652-fc3ebd901c08/go.mod h1:9qZd/S8DqWzfKtjKGgSoHqGEByYmUE3qRaBaaAHwfEM=
+github.com/rancher/lasso v0.0.0-20210408231703-9ddd9378d08d h1:vfjPEF6M7Jf1/zK1xF7z2drLfniooKcgDQdoXO5+U7w=
+github.com/rancher/lasso v0.0.0-20210408231703-9ddd9378d08d/go.mod h1:OhBBBO1pBwYp0hacWdnvSGOj+XE9yMLOLnaypIlic18=
 github.com/rancher/wrangler v0.6.1/go.mod h1:L4HtjPeX8iqLgsxfJgz+JjKMcX2q3qbRXSeTlC/CSd4=
 github.com/rancher/wrangler v0.6.2-0.20200829053106-7e1dd4260224/go.mod h1:I7qe4DZNMOLKVa9ax7DJdBZ0XtKOppLF/dalhPX3vaE=
 github.com/rancher/wrangler v0.7.3-0.20201002224307-4303c423125a/go.mod h1:goezjesEKwMxHLfltdjg9DW0xWV7txQee6vOuSDqXAI=
-github.com/rancher/wrangler v0.8.4-0.20210721232424-82b1bc4f2de5 h1:irIeJbyOWB/RKWoRpdgZH8OtQSC/4YpxMBB71dqnOU0=
-github.com/rancher/wrangler v0.8.4-0.20210721232424-82b1bc4f2de5/go.mod h1:dKEaHNB4izxmPUtpq1Hvr3z3Oh+9k5pCZyFO9sUhlaY=
+github.com/rancher/wrangler v0.8.1-0.20210521213200-39dd8bf93e9f h1:3LJN3poWNzVHPpbbsCnFih4ujSGYCyVnG5+wZ9kXwGs=
+github.com/rancher/wrangler v0.8.1-0.20210521213200-39dd8bf93e9f/go.mod h1:aj/stIidTzU6UEKKRB8JyrrqNMJAfDMziL1+zhG8lc0=
 github.com/rancher/wrangler-cli v0.0.0-20200815040857-81c48cf8ab43 h1:+Bc9QnL9GuZiYxc3Mvm4n6EEjgI5TFQLDGjpRhQbkVk=
 github.com/rancher/wrangler-cli v0.0.0-20200815040857-81c48cf8ab43/go.mod h1:KxpGNhk/oVL6LCfyxESTD1sb8eXRlUxtkbNm06+7dZU=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
diff --git a/modules/agent/pkg/controllers/controllers.go b/modules/agent/pkg/controllers/controllers.go
index 11cca0ec..5717ca85 100644
--- a/modules/agent/pkg/controllers/controllers.go
+++ b/modules/agent/pkg/controllers/controllers.go
@@ -94,7 +94,7 @@ func Register(ctx context.Context, leaderElect bool,
 	}
 
 	helmDeployer, err := helmdeployer.NewHelm(agentNamespace, defaultNamespace, labelPrefix, appCtx,
-		appCtx.Core.ServiceAccount().Cache())
+		appCtx.Core.ServiceAccount().Cache(), appCtx.Core.ConfigMap().Cache(), appCtx.Core.Secret().Cache())
 	if err != nil {
 		return err
 	}
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go b/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go
index a9288d70..882bd3ff 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/bundle.go
@@ -227,12 +227,43 @@ type HelmOptions struct {
 	Version        string       `json:"version,omitempty"`
 	TimeoutSeconds int          `json:"timeoutSeconds,omitempty"`
 	Values         *GenericMap  `json:"values,omitempty"`
+	ValuesFrom     []ValuesFrom `json:"valuesFrom,omitempty"`
 	Force          bool         `json:"force,omitempty"`
 	TakeOwnership  bool         `json:"takeOwnership,omitempty"`
 	MaxHistory     int          `json:"maxHistory,omitempty"`
 	ValuesFiles    []string     `json:"valuesFiles,omitempty"`
 }
 
+// Define helm values that can come from configmap, secret or external. Credit: https://github.com/fluxcd/helm-operator/blob/0cfea875b5d44bea995abe7324819432070dfbdc/pkg/apis/helm.fluxcd.io/v1/types_helmrelease.go#L439
+type ValuesFrom struct {
+	// The reference to a config map with release values.
+	// +optional
+	ConfigMapKeyRef *ConfigMapKeySelector `json:"configMapKeyRef,omitempty"`
+	// The reference to a secret with release values.
+	// +optional
+	SecretKeyRef *SecretKeySelector `json:"secretKeyRef,omitempty"`
+}
+
+type ConfigMapKeySelector struct {
+	LocalObjectReference `json:",inline"`
+	// +optional
+	Namespace string `json:"namespace,omitempty"`
+	// +optional
+	Key string `json:"key,omitempty"`
+}
+
+type SecretKeySelector struct {
+	LocalObjectReference `json:",inline"`
+	// +optional
+	Namespace string `json:"namespace,omitempty"`
+	// +optional
+	Key string `json:"key,omitempty"`
+}
+
+type LocalObjectReference struct {
+	Name string `json:"name"`
+}
+
 type BundleDeploymentSpec struct {
 	StagedOptions      BundleDeploymentOptions `json:"stagedOptions,omitempty"`
 	StagedDeploymentID string                  `json:"stagedDeploymentID,omitempty"`
diff --git a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go
index 759159f8..c0305841 100644
--- a/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go
+++ b/pkg/apis/fleet.cattle.io/v1alpha1/zz_generated_deepcopy.go
@@ -1082,6 +1082,23 @@ func (in *ComparePatch) DeepCopy() *ComparePatch {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ConfigMapKeySelector) DeepCopyInto(out *ConfigMapKeySelector) {
+	*out = *in
+	out.LocalObjectReference = in.LocalObjectReference
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapKeySelector.
+func (in *ConfigMapKeySelector) DeepCopy() *ConfigMapKeySelector {
+	if in == nil {
+		return nil
+	}
+	out := new(ConfigMapKeySelector)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Content) DeepCopyInto(out *Content) {
 	*out = *in
@@ -1483,6 +1500,13 @@ func (in *HelmOptions) DeepCopyInto(out *HelmOptions) {
 		in, out := &in.Values, &out.Values
 		*out = (*in).DeepCopy()
 	}
+	if in.ValuesFrom != nil {
+		in, out := &in.ValuesFrom, &out.ValuesFrom
+		*out = make([]ValuesFrom, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 	if in.ValuesFiles != nil {
 		in, out := &in.ValuesFiles, &out.ValuesFiles
 		*out = make([]string, len(*in))
@@ -1649,6 +1673,22 @@ func (in *KustomizeOptions) DeepCopy() *KustomizeOptions {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LocalObjectReference) DeepCopyInto(out *LocalObjectReference) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalObjectReference.
+func (in *LocalObjectReference) DeepCopy() *LocalObjectReference {
+	if in == nil {
+		return nil
+	}
+	out := new(LocalObjectReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ModifiedStatus) DeepCopyInto(out *ModifiedStatus) {
 	*out = *in
@@ -1848,6 +1888,23 @@ func (in *RolloutStrategy) DeepCopy() *RolloutStrategy {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) {
+	*out = *in
+	out.LocalObjectReference = in.LocalObjectReference
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector.
+func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
+	if in == nil {
+		return nil
+	}
+	out := new(SecretKeySelector)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SemVerPolicy) DeepCopyInto(out *SemVerPolicy) {
 	*out = *in
@@ -1864,6 +1921,32 @@ func (in *SemVerPolicy) DeepCopy() *SemVerPolicy {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ValuesFrom) DeepCopyInto(out *ValuesFrom) {
+	*out = *in
+	if in.ConfigMapKeyRef != nil {
+		in, out := &in.ConfigMapKeyRef, &out.ConfigMapKeyRef
+		*out = new(ConfigMapKeySelector)
+		**out = **in
+	}
+	if in.SecretKeyRef != nil {
+		in, out := &in.SecretKeyRef, &out.SecretKeyRef
+		*out = new(SecretKeySelector)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValuesFrom.
+func (in *ValuesFrom) DeepCopy() *ValuesFrom {
+	if in == nil {
+		return nil
+	}
+	out := new(ValuesFrom)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *YAMLOptions) DeepCopyInto(out *YAMLOptions) {
 	*out = *in
diff --git a/pkg/controllers/cluster/import.go b/pkg/controllers/cluster/import.go
index 0ed672b0..5de28f2e 100644
--- a/pkg/controllers/cluster/import.go
+++ b/pkg/controllers/cluster/import.go
@@ -18,10 +18,8 @@ import (
 	"github.com/rancher/wrangler/pkg/randomtoken"
 	"github.com/rancher/wrangler/pkg/yaml"
 	"github.com/sirupsen/logrus"
-	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
@@ -31,7 +29,6 @@ var (
 	ImportTokenPrefix = "import-token-"
 	ImportTokenTTL    = 12 * time.Hour
 	t                 = true
-	NamespaceKind     = "Namespace"
 )
 
 type importHandler struct {
@@ -189,11 +186,7 @@ func (i *importHandler) importCluster(cluster *fleet.Cluster, status fleet.Clust
 	if err != nil {
 		return status, err
 	}
-	apply = apply.WithDynamicLookup().WithSetID("fleet-agent-bootstrap").WithNoDeleteGVK(schema.GroupVersionKind{
-		Group:   corev1.SchemeGroupVersion.Group,
-		Version: corev1.SchemeGroupVersion.Version,
-		Kind:    NamespaceKind,
-	})
+	apply = apply.WithDynamicLookup().WithSetID("fleet-agent-bootstrap")
 
 	token, err := i.tokens.Get(cluster.Namespace, ImportTokenPrefix+cluster.Name)
 	if err != nil {
diff --git a/pkg/helmdeployer/deployer.go b/pkg/helmdeployer/deployer.go
index 9939556e..2f16f4b1 100644
--- a/pkg/helmdeployer/deployer.go
+++ b/pkg/helmdeployer/deployer.go
@@ -26,6 +26,7 @@ import (
 	"helm.sh/helm/v3/pkg/kube"
 	"helm.sh/helm/v3/pkg/release"
 	"helm.sh/helm/v3/pkg/storage/driver"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 )
@@ -55,6 +56,8 @@ type postRender struct {
 type helm struct {
 	agentNamespace      string
 	serviceAccountCache corecontrollers.ServiceAccountCache
+	configmapCache      corecontrollers.ConfigMapCache
+	secretCache         corecontrollers.SecretCache
 	getter              genericclioptions.RESTClientGetter
 	globalCfg           action.Configuration
 	useGlobalCfg        bool
@@ -64,12 +67,14 @@ type helm struct {
 }
 
 func NewHelm(namespace, defaultNamespace, labelPrefix string, getter genericclioptions.RESTClientGetter,
-	serviceAccountCache corecontrollers.ServiceAccountCache) (deployer.Deployer, error) {
+	serviceAccountCache corecontrollers.ServiceAccountCache, configmapCache corecontrollers.ConfigMapCache, secretCache corecontrollers.SecretCache) (deployer.Deployer, error) {
 	h := &helm{
 		getter:              getter,
 		defaultNamespace:    defaultNamespace,
 		agentNamespace:      namespace,
 		serviceAccountCache: serviceAccountCache,
+		configmapCache:      configmapCache,
+		secretCache:         secretCache,
 		labelPrefix:         labelPrefix,
 	}
 	if err := h.globalCfg.Init(getter, "", "secrets", logrus.Infof); err != nil {
@@ -228,13 +233,6 @@ func (h *helm) getOpts(bundleID string, options fleet.BundleDeploymentOptions) (
 	return timeout, options.DefaultNamespace, releaseName
 }
 
-func (h *helm) getValues(options fleet.BundleDeploymentOptions) map[string]interface{} {
-	if options.Helm != nil && options.Helm.Values != nil {
-		return options.Helm.Values.Data
-	}
-	return nil
-}
-
 func (h *helm) getCfg(namespace, serviceAccountName string) (action.Configuration, error) {
 	var (
 		cfg    action.Configuration
@@ -270,7 +268,10 @@ func (h *helm) getCfg(namespace, serviceAccountName string) (action.Configuratio
 func (h *helm) install(bundleID string, manifest *manifest.Manifest, chart *chart.Chart, options fleet.BundleDeploymentOptions, dryRun bool) (*release.Release, error) {
 	timeout, defaultNamespace, releaseName := h.getOpts(bundleID, options)
 
-	values := h.getValues(options)
+	values, err := h.getValues(options, defaultNamespace)
+	if err != nil {
+		return nil, err
+	}
 
 	cfg, err := h.getCfg(defaultNamespace, options.ServiceAccount)
 	if err != nil {
@@ -353,6 +354,66 @@ func (h *helm) install(bundleID string, manifest *manifest.Manifest, chart *char
 	return u.Run(releaseName, chart, values)
 }
 
+func (h *helm) getValues(options fleet.BundleDeploymentOptions, defaultNamespace string) (map[string]interface{}, error) {
+	if options.Helm == nil {
+		return nil, nil
+	}
+
+	var values map[string]interface{}
+	if options.Helm.Values != nil {
+		values = options.Helm.Values.Data
+	}
+
+	// do not run this when using template
+	if !h.template {
+		for _, valuesFrom := range options.Helm.ValuesFrom {
+			var tempValues map[string]interface{}
+			if valuesFrom.SecretKeyRef != nil {
+				name := valuesFrom.SecretKeyRef.Name
+				namespace := valuesFrom.SecretKeyRef.Namespace
+				if namespace == "" {
+					namespace = defaultNamespace
+				}
+				key := valuesFrom.SecretKeyRef.Key
+				if key == "" {
+					key = DefaultKey
+				}
+				secret, err := h.secretCache.Get(namespace, name)
+				if err != nil {
+					return nil, err
+				}
+				tempValues, err = processValuesFromObject(name, namespace, key, secret, nil)
+				if err != nil {
+					return nil, err
+				}
+			} else if valuesFrom.ConfigMapKeyRef != nil {
+				name := valuesFrom.ConfigMapKeyRef.Name
+				namespace := valuesFrom.ConfigMapKeyRef.Namespace
+				if namespace == "" {
+					namespace = defaultNamespace
+				}
+				key := valuesFrom.ConfigMapKeyRef.Key
+				if key == "" {
+					key = DefaultKey
+				}
+				configMap, err := h.configmapCache.Get(namespace, name)
+				if err != nil {
+					return nil, err
+				}
+				tempValues, err = processValuesFromObject(name, namespace, key, nil, configMap)
+				if err != nil {
+					return nil, err
+				}
+			}
+			if tempValues != nil {
+				values = mergeValues(values, tempValues)
+			}
+		}
+	}
+
+	return values, nil
+}
+
 func (h *helm) ListDeployments() ([]deployer.DeployedBundle, error) {
 	list := action.NewList(&h.globalCfg)
 	list.All = true
@@ -552,6 +613,28 @@ func deleteHistory(cfg action.Configuration, bundleID string) error {
 	return nil
 }
 
+func processValuesFromObject(name, namespace, key string, secret *corev1.Secret, configMap *corev1.ConfigMap) (map[string]interface{}, error) {
+	var m map[string]interface{}
+	if secret != nil {
+		values, ok := secret.Data[key]
+		if !ok {
+			return nil, fmt.Errorf("key %s is missing from secret %s/%s, can't use it in valuesFrom", key, namespace, name)
+		}
+		if err := yaml.Unmarshal(values, &m); err != nil {
+			return nil, err
+		}
+	} else if configMap != nil {
+		values, ok := configMap.Data[key]
+		if !ok {
+			return nil, fmt.Errorf("key %s is missing from configmap %s/%s, can't use it in valuesFrom", key, namespace, name)
+		}
+		if err := yaml.Unmarshal([]byte(values), &m); err != nil {
+			return nil, err
+		}
+	}
+	return m, nil
+}
+
 func mergeMaps(base, other map[string]string) map[string]string {
 	result := map[string]string{}
 	for k, v := range base {
diff --git a/pkg/options/calculate.go b/pkg/options/calculate.go
index e66032de..79cf36e9 100644
--- a/pkg/options/calculate.go
+++ b/pkg/options/calculate.go
@@ -64,6 +64,9 @@ func merge(base, next fleet.BundleDeploymentOptions) fleet.BundleDeploymentOptio
 		} else if next.Helm.Values != nil {
 			result.Helm.Values.Data = data.MergeMaps(result.Helm.Values.Data, next.Helm.Values.Data)
 		}
+		if next.Helm.ValuesFrom != nil {
+			result.Helm.ValuesFrom = append(result.Helm.ValuesFrom, next.Helm.ValuesFrom...)
+		}
 		if next.Helm.Chart != "" {
 			result.Helm.Chart = next.Helm.Chart
 		}


Test patch:
diff --git a/pkg/helmdeployer/deployer_test.go b/pkg/helmdeployer/deployer_test.go
new file mode 100644
index 00000000..b7ad04c1
--- /dev/null
+++ b/pkg/helmdeployer/deployer_test.go
@@ -0,0 +1,61 @@
+package helmdeployer
+
+import (
+	"fmt"
+	"runtime"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func TestValuesFrom(t *testing.T) {
+	a := assert.New(t)
+	key := "values.yaml"
+	newline := "\n"
+	if runtime.GOOS == "windows" {
+		newline = "\r\n"
+	}
+
+	configMapPayload := fmt.Sprintf("replication: \"true\"%sreplicas: \"2\"%sserviceType: NodePort", newline, newline)
+	secretPayload := fmt.Sprintf("replication: \"false\"%sreplicas: \"3\"%sserviceType: NodePort%sfoo: bar", newline, newline, newline)
+	totalValues := map[string]interface{}{"beforeMerge": "value"}
+	expected := map[string]interface{}{
+		"beforeMerge": "value",
+		"replicas":    "2",
+		"replication": "true",
+		"serviceType": "NodePort",
+		"foo":         "bar",
+	}
+
+	configMapName := "configmap-name"
+	configMapNamespace := "configmap-namespace"
+	configMapValues, err := processValuesFromObject(configMapName, configMapNamespace, key, nil, &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      configMapName,
+			Namespace: configMapNamespace,
+		},
+		Data: map[string]string{
+			key: configMapPayload,
+		},
+	})
+	a.NoError(err)
+
+	secretName := "secret-name"
+	secretNamespace := "secret-namespace"
+	secretValues, err := processValuesFromObject(secretName, secretNamespace, key, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      secretName,
+			Namespace: secretNamespace,
+		},
+		Data: map[string][]byte{
+			key: []byte(secretPayload),
+		},
+	}, nil)
+	a.NoError(err)
+
+	totalValues = mergeValues(totalValues, secretValues)
+	totalValues = mergeValues(totalValues, configMapValues)
+	a.Equal(expected, totalValues)
+}