Repository: replicatedhq/kots (go)
Issue: Template functions related to local registry no longer work in Config resource
With KOTS 1.18.1, I was using a pattern like
```
apiVersion: kots.io/v1beta1
kind: Config
metadata:
  name: cd4pe-config
spec:
  groups:
  - name: setup
    title: Setup
    items:
    - name: registry_hostname
      type: text
      title: Local registry hostname
      required: true
      when: '{{repl and IsKurl HasLocalRegistry}}'
    - name: registry_endpoint
      type: text
      title: Local registry endpoint
      default: repl{{ConfigOption "registry_hostname"}}
      when: '{{repl and IsKurl HasLocalRegistry}}'
```
to provide config for using the local registry kURL sets up in an airgap install. When I upgrade to KOTS 1.19.1 (or later), `HasLocalRegistry` now always returns false and these config items no longer show up. It continues to work correctly when deploying resources.

This seems to be a regression around how KOTS renders Config in the Config tab.
Pull request description: Provide registry info for template builder
Closes #1126

Patch:
diff --git a/kotsadm/pkg/airgap/airgap.go b/kotsadm/pkg/airgap/airgap.go
index b612738ed..d60eefe42 100644
--- a/kotsadm/pkg/airgap/airgap.go
+++ b/kotsadm/pkg/airgap/airgap.go
@@ -246,7 +246,17 @@ func CreateAppFromAirgap(pendingApp *types.PendingApp, airgapPath string, regist
 			return errors.Wrap(err, "failed to marshal configvalues spec")
 		}
 
-		needsConfig, err := kotsadmconfig.NeedsConfiguration(configSpec, configValuesSpec, licenseSpec)
+		configOpts := kotsadmconfig.ConfigOptions{
+			ConfigSpec:        configSpec,
+			ConfigValuesSpec:  configValuesSpec,
+			LicenseSpec:       licenseSpec,
+			RegistryHost:      registryHost,
+			RegistryNamespace: namespace,
+			RegistryUser:      username,
+			RegistryPassword:  password,
+		}
+
+		needsConfig, err := kotsadmconfig.NeedsConfiguration(configOpts)
 		if err != nil {
 			return errors.Wrap(err, "failed to check if app needs configuration")
 		}
diff --git a/kotsadm/pkg/config/config.go b/kotsadm/pkg/config/config.go
index d76e75a9b..9864545ea 100644
--- a/kotsadm/pkg/config/config.go
+++ b/kotsadm/pkg/config/config.go
@@ -17,6 +17,16 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
 )
 
+type ConfigOptions struct {
+	ConfigSpec        string
+	ConfigValuesSpec  string
+	LicenseSpec       string
+	RegistryHost      string
+	RegistryNamespace string
+	RegistryUser      string
+	RegistryPassword  string
+}
+
 func IsRequiredItem(item kotsv1beta1.ConfigItem) bool {
 	if !item.Required {
 		return false
@@ -37,13 +47,19 @@ func IsUnsetItem(item kotsv1beta1.ConfigItem) bool {
 	return true
 }
 
-func NeedsConfiguration(configSpec string, configValuesSpec string, licenseSpec string) (bool, error) {
-	if configSpec == "" {
+func NeedsConfiguration(opts ConfigOptions) (bool, error) {
+	if opts.ConfigSpec == "" {
 		return false, nil
 	}
 
-	localRegistry := template.LocalRegistry{}
-	rendered, err := kotsconfig.TemplateConfig(logger.NewLogger(), configSpec, configValuesSpec, licenseSpec, localRegistry)
+	localRegistry := template.LocalRegistry{
+		Host:      opts.RegistryHost,
+		Namespace: opts.RegistryNamespace,
+		Username:  opts.RegistryUser,
+		Password:  opts.RegistryPassword,
+	}
+
+	rendered, err := kotsconfig.TemplateConfig(logger.NewLogger(), opts.ConfigSpec, opts.ConfigValuesSpec, opts.LicenseSpec, localRegistry)
 	if err != nil {
 		return false, errors.Wrap(err, "failed to template config")
 	}
diff --git a/kotsadm/pkg/handlers/config.go b/kotsadm/pkg/handlers/config.go
index 7a5b1559b..546681384 100644
--- a/kotsadm/pkg/handlers/config.go
+++ b/kotsadm/pkg/handlers/config.go
@@ -200,8 +200,23 @@ func LiveAppConfig(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	registryInfo, err := store.GetStore().GetRegistryDetailsForApp(foundApp.ID)
+	if err != nil {
+		liveAppConfigResponse.Error = "failed to get app registry info"
+		JSON(w, http.StatusInternalServerError, liveAppConfigResponse)
+		return
+	}
+
+	localRegistry := template.LocalRegistry{}
+	if registryInfo != nil {
+		localRegistry.Host = registryInfo.Hostname
+		localRegistry.Namespace = registryInfo.Namespace
+		localRegistry.Username = registryInfo.Username
+		localRegistry.Password = registryInfo.Password
+	}
+
 	versionInfo := template.VersionInfoFromInstallation(liveAppConfigRequest.Sequence+1, foundApp.IsAirgap, kotsKinds.Installation.Spec) // sequence +1 because the sequence will be incremented on save (and we want the preview to be accurate)
-	renderedConfig, err := kotsconfig.TemplateConfigObjects(kotsKinds.Config, configValues, appLicense, template.LocalRegistry{}, &versionInfo)
+	renderedConfig, err := kotsconfig.TemplateConfigObjects(kotsKinds.Config, configValues, appLicense, localRegistry, &versionInfo)
 	if err != nil {
 		liveAppConfigResponse.Error = "failed to render templates"
 		JSON(w, http.StatusInternalServerError, liveAppConfigResponse)
@@ -266,8 +281,23 @@ func CurrentAppConfig(w http.ResponseWriter, r *http.Request) {
 		configValues[key] = generatedValue
 	}
 
+	registryInfo, err := store.GetStore().GetRegistryDetailsForApp(foundApp.ID)
+	if err != nil {
+		currentAppConfigResponse.Error = "failed to get app registry info"
+		JSON(w, http.StatusInternalServerError, currentAppConfigResponse)
+		return
+	}
+
+	localRegistry := template.LocalRegistry{}
+	if registryInfo != nil {
+		localRegistry.Host = registryInfo.Hostname
+		localRegistry.Namespace = registryInfo.Namespace
+		localRegistry.Username = registryInfo.Username
+		localRegistry.Password = registryInfo.Password
+	}
+
 	versionInfo := template.VersionInfoFromInstallation(int64(sequence)+1, foundApp.IsAirgap, kotsKinds.Installation.Spec) // sequence +1 because the sequence will be incremented on save (and we want the preview to be accurate)
-	renderedConfig, err := kotsconfig.TemplateConfigObjects(kotsKinds.Config, configValues, appLicense, template.LocalRegistry{}, &versionInfo)
+	renderedConfig, err := kotsconfig.TemplateConfigObjects(kotsKinds.Config, configValues, appLicense, localRegistry, &versionInfo)
 	if err != nil {
 		currentAppConfigResponse.Error = "failed to render templates"
 		JSON(w, http.StatusInternalServerError, currentAppConfigResponse)
diff --git a/kotsadm/pkg/online/online.go b/kotsadm/pkg/online/online.go
index 9aa279ee3..174473b51 100644
--- a/kotsadm/pkg/online/online.go
+++ b/kotsadm/pkg/online/online.go
@@ -176,7 +176,13 @@ func CreateAppFromOnline(pendingApp *types.PendingApp, upstreamURI string, isAut
 			return nil, errors.Wrap(err, "failed to marshal configvalues spec")
 		}
 
-		needsConfig, err := kotsadmconfig.NeedsConfiguration(configSpec, configValuesSpec, licenseSpec)
+		configOptions := kotsadmconfig.ConfigOptions{
+			ConfigSpec:       configSpec,
+			ConfigValuesSpec: configValuesSpec,
+			LicenseSpec:      licenseSpec,
+			// TODO: are there ever registry settings here?
+		}
+		needsConfig, err := kotsadmconfig.NeedsConfiguration(configOptions)
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to check if app needs configuration")
 		}
diff --git a/kotsadm/pkg/store/ocistore/version_store.go b/kotsadm/pkg/store/ocistore/version_store.go
index 2c1c2a081..d2ba3b780 100644
--- a/kotsadm/pkg/store/ocistore/version_store.go
+++ b/kotsadm/pkg/store/ocistore/version_store.go
@@ -319,6 +319,11 @@ func (s OCIStore) CreateAppVersion(appID string, currentSequence *int64, appName
 		previousArchiveDir = previousDir
 	}
 
+	registryInfo, err := s.GetRegistryDetailsForApp(appID)
+	if err != nil {
+		return int64(0), errors.Wrap(err, "failed to get app registry info")
+	}
+
 	downstreams, err := s.ListDownstreamsForApp(appID)
 	if err != nil {
 		return int64(0), errors.Wrap(err, "failed to list downstreams")
@@ -348,8 +353,20 @@ func (s OCIStore) CreateAppVersion(appID string, currentSequence *int64, appName
 				return int64(0), errors.Wrap(err, "failed to marshal configvalues spec")
 			}
 
+			configOpts := kotsconfig.ConfigOptions{
+				ConfigSpec:       configSpec,
+				ConfigValuesSpec: configValuesSpec,
+				LicenseSpec:      licenseSpec,
+			}
+			if registryInfo != nil {
+				configOpts.RegistryHost = registryInfo.Hostname
+				configOpts.RegistryNamespace = registryInfo.Namespace
+				configOpts.RegistryUser = registryInfo.Username
+				configOpts.RegistryPassword = registryInfo.Password
+			}
+
 			// check if version needs additional configuration
-			t, err := kotsconfig.NeedsConfiguration(configSpec, configValuesSpec, licenseSpec)
+			t, err := kotsconfig.NeedsConfiguration(configOpts)
 			if err != nil {
 				return int64(0), errors.Wrap(err, "failed to check if version needs configuration")
 			}
diff --git a/kotsadm/pkg/store/s3pg/version_store.go b/kotsadm/pkg/store/s3pg/version_store.go
index e354165e3..845dd4b7c 100644
--- a/kotsadm/pkg/store/s3pg/version_store.go
+++ b/kotsadm/pkg/store/s3pg/version_store.go
@@ -300,6 +300,11 @@ func (s S3PGStore) CreateAppVersion(appID string, currentSequence *int64, appNam
 		previousArchiveDir = previousDir
 	}
 
+	registryInfo, err := s.GetRegistryDetailsForApp(appID)
+	if err != nil {
+		return int64(0), errors.Wrap(err, "failed to get app registry info")
+	}
+
 	downstreams, err := s.ListDownstreamsForApp(appID)
 	if err != nil {
 		return int64(0), errors.Wrap(err, "failed to list downstreams")
@@ -329,8 +334,20 @@ func (s S3PGStore) CreateAppVersion(appID string, currentSequence *int64, appNam
 				return int64(0), errors.Wrap(err, "failed to marshal configvalues spec")
 			}
 
+			configOpts := kotsconfig.ConfigOptions{
+				ConfigSpec:       configSpec,
+				ConfigValuesSpec: configValuesSpec,
+				LicenseSpec:      licenseSpec,
+			}
+			if registryInfo != nil {
+				configOpts.RegistryHost = registryInfo.Hostname
+				configOpts.RegistryNamespace = registryInfo.Namespace
+				configOpts.RegistryUser = registryInfo.Username
+				configOpts.RegistryPassword = registryInfo.Password
+			}
+
 			// check if version needs additional configuration
-			t, err := kotsconfig.NeedsConfiguration(configSpec, configValuesSpec, licenseSpec)
+			t, err := kotsconfig.NeedsConfiguration(configOpts)
 			if err != nil {
 				return int64(0), errors.Wrap(err, "failed to check if version needs configuration")
 			}
diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go
index dfb43ad69..f012b911b 100644
--- a/pkg/pull/pull.go
+++ b/pkg/pull/pull.go
@@ -109,13 +109,20 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 		return "", errors.Wrap(err, "failed to parse uri")
 	}
 
-	fetchOptions := upstream.FetchOptions{}
-	fetchOptions.HelmRepoURI = pullOptions.HelmRepoURI
-	fetchOptions.RootDir = pullOptions.RootDir
-	fetchOptions.UseAppDir = pullOptions.CreateAppDir
-	fetchOptions.LocalPath = pullOptions.LocalPath
-	fetchOptions.CurrentCursor = pullOptions.UpdateCursor
-	fetchOptions.AppSequence = pullOptions.AppSequence
+	fetchOptions := upstream.FetchOptions{
+		HelmRepoURI:   pullOptions.HelmRepoURI,
+		RootDir:       pullOptions.RootDir,
+		UseAppDir:     pullOptions.CreateAppDir,
+		LocalPath:     pullOptions.LocalPath,
+		CurrentCursor: pullOptions.UpdateCursor,
+		AppSequence:   pullOptions.AppSequence,
+		LocalRegistry: upstream.LocalRegistry{
+			Host:      pullOptions.RewriteImageOptions.Host,
+			Namespace: pullOptions.RewriteImageOptions.Namespace,
+			Username:  pullOptions.RewriteImageOptions.Username,
+			Password:  pullOptions.RewriteImageOptions.Password,
+		},
+	}
 
 	var installation *kotsv1beta1.Installation
 
diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go
index 9cbda7b1e..7ffc8b88b 100644
--- a/pkg/rewrite/rewrite.go
+++ b/pkg/rewrite/rewrite.go
@@ -64,6 +64,12 @@ func Rewrite(rewriteOptions RewriteOptions) error {
 		CurrentVersionLabel: rewriteOptions.Installation.Spec.VersionLabel,
 		EncryptionKey:       rewriteOptions.Installation.Spec.EncryptionKey,
 		License:             rewriteOptions.License,
+		LocalRegistry: upstream.LocalRegistry{
+			Host:      rewriteOptions.RegistryEndpoint,
+			Namespace: rewriteOptions.RegistryNamespace,
+			Username:  rewriteOptions.RegistryUsername,
+			Password:  rewriteOptions.RegistryPassword,
+		},
 	}
 
 	log.ActionWithSpinner("Pulling upstream")
diff --git a/pkg/upstream/fetch.go b/pkg/upstream/fetch.go
index 583d481de..bcbd8ef5b 100644
--- a/pkg/upstream/fetch.go
+++ b/pkg/upstream/fetch.go
@@ -27,6 +27,14 @@ type FetchOptions struct {
 	DownstreamCursor    string
 	DownstreamChannel   string
 	AppSequence         int64
+	LocalRegistry       LocalRegistry
+}
+
+type LocalRegistry struct {
+	Host      string
+	Namespace string
+	Username  string
+	Password  string
 }
 
 func FetchUpstream(upstreamURI string, fetchOptions *FetchOptions) (*types.Upstream, error) {
@@ -60,7 +68,7 @@ func downloadUpstream(upstreamURI string, fetchOptions *FetchOptions) (*types.Up
 		return downloadHelm(u, fetchOptions.HelmRepoURI)
 	}
 	if u.Scheme == "replicated" {
-		return downloadReplicated(u, fetchOptions.LocalPath, fetchOptions.RootDir, fetchOptions.UseAppDir, fetchOptions.License, fetchOptions.ConfigValues, pickCursor(fetchOptions), pickVersionLabel(fetchOptions), cipher, fetchOptions.AppSequence, fetchOptions.Airgap != nil)
+		return downloadReplicated(u, fetchOptions.LocalPath, fetchOptions.RootDir, fetchOptions.UseAppDir, fetchOptions.License, fetchOptions.ConfigValues, pickCursor(fetchOptions), pickVersionLabel(fetchOptions), cipher, fetchOptions.AppSequence, fetchOptions.Airgap != nil, fetchOptions.LocalRegistry)
 	}
 	if u.Scheme == "git" {
 		return downloadGit(upstreamURI)
diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go
index b80a6871f..bfe44e223 100644
--- a/pkg/upstream/replicated.go
+++ b/pkg/upstream/replicated.go
@@ -114,7 +114,7 @@ func getUpdatesReplicated(u *url.URL, localPath string, currentCursor Replicated
 	return updates, nil
 }
 
-func downloadReplicated(u *url.URL, localPath string, rootDir string, useAppDir bool, license *kotsv1beta1.License, existingConfigValues *kotsv1beta1.ConfigValues, updateCursor ReplicatedCursor, versionLabel string, cipher *crypto.AESCipher, appSequence int64, isAirgap bool) (*types.Upstream, error) {
+func downloadReplicated(u *url.URL, localPath string, rootDir string, useAppDir bool, license *kotsv1beta1.License, existingConfigValues *kotsv1beta1.ConfigValues, updateCursor ReplicatedCursor, versionLabel string, cipher *crypto.AESCipher, appSequence int64, isAirgap bool, registry LocalRegistry) (*types.Upstream, error) {
 	var release *Release
 
 	if localPath != "" {
@@ -196,9 +196,16 @@ func downloadReplicated(u *url.URL, localPath string, rootDir string, useAppDir
 			IsAirgap:     isAirgap,
 		}
 
+		localRegistry := template.LocalRegistry{
+			Host:      registry.Host,
+			Namespace: registry.Namespace,
+			Username:  registry.Username,
+			Password:  registry.Password,
+		}
+
 		// If config existed and was removed from the app,
 		// values will be carried over to the new version anyway.
-		configValues, err := createConfigValues(application.Name, config, existingConfigValues, cipher, license, &versionInfo)
+		configValues, err := createConfigValues(application.Name, config, existingConfigValues, cipher, license, &versionInfo, localRegistry)
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to create empty config values")
 		}
@@ -499,7 +506,7 @@ func mustMarshalConfigValues(configValues *kotsv1beta1.ConfigValues) []byte {
 	return b.Bytes()
 }
 
-func createConfigValues(applicationName string, config *kotsv1beta1.Config, existingConfigValues *kotsv1beta1.ConfigValues, cipher *crypto.AESCipher, license *kotsv1beta1.License, versionInfo *template.VersionInfo) (*kotsv1beta1.ConfigValues, error) {
+func createConfigValues(applicationName string, config *kotsv1beta1.Config, existingConfigValues *kotsv1beta1.ConfigValues, cipher *crypto.AESCipher, license *kotsv1beta1.License, versionInfo *template.VersionInfo, localRegistry template.LocalRegistry) (*kotsv1beta1.ConfigValues, error) {
 	templateContextValues := make(map[string]template.ItemValue)
 
 	var newValues kotsv1beta1.ConfigValuesSpec
@@ -536,11 +543,6 @@ func createConfigValues(applicationName string, config *kotsv1beta1.Config, exis
 		}, nil
 	}
 
-	// Today, these aren't needed in this function
-	// They are needed in rendering the base
-	// We should get this supported before 1.13.0 ships
-	localRegistry := template.LocalRegistry{}
-
 	builder, _, err := template.NewBuilder(config.Spec.Groups, templateContextValues, localRegistry, cipher, license, versionInfo)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to create config context")


Test patch:
diff --git a/pkg/upstream/replicated_test.go b/pkg/upstream/replicated_test.go
index 664e97dc1..7f830d9a0 100644
--- a/pkg/upstream/replicated_test.go
+++ b/pkg/upstream/replicated_test.go
@@ -6,6 +6,7 @@ import (
 
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	"github.com/replicatedhq/kots/kotskinds/multitype"
+	"github.com/replicatedhq/kots/pkg/template"
 	"github.com/replicatedhq/kots/pkg/upstream/types"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -249,13 +250,13 @@ func Test_createConfigValues(t *testing.T) {
 			Default: "default_4",
 		},
 	}
-	values1, err := createConfigValues(applicationName, config, nil, nil, nil, nil)
+	values1, err := createConfigValues(applicationName, config, nil, nil, nil, nil, template.LocalRegistry{})
 	req.NoError(err)
 	assert.Equal(t, expected1, values1.Spec.Values)
 
 	// Like an app without a config, should have exact same values
 	expected2 := configValues.Spec.Values
-	values2, err := createConfigValues(applicationName, nil, configValues, nil, nil, nil)
+	values2, err := createConfigValues(applicationName, nil, configValues, nil, nil, nil, template.LocalRegistry{})
 	req.NoError(err)
 	assert.Equal(t, expected2, values2.Spec.Values)
 
@@ -276,7 +277,7 @@ func Test_createConfigValues(t *testing.T) {
 			Default: "default_4",
 		},
 	}
-	values3, err := createConfigValues(applicationName, config, configValues, nil, nil, nil)
+	values3, err := createConfigValues(applicationName, config, configValues, nil, nil, nil, template.LocalRegistry{})
 	req.NoError(err)
 	assert.Equal(t, expected3, values3.Spec.Values)
 }