Repository: replicatedhq/kots
Pull request #899 (2020-08-03 22:14:35)

Do not fail on license sync error on application updates


Patch:
diff --git a/kotsadm/pkg/handlers/license.go b/kotsadm/pkg/handlers/license.go
index df1a83033..584ff326d 100644
--- a/kotsadm/pkg/handlers/license.go
+++ b/kotsadm/pkg/handlers/license.go
@@ -114,7 +114,7 @@ func SyncLicense(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	latestLicense, err := license.Sync(foundApp, syncLicenseRequest.LicenseData)
+	latestLicense, err := license.Sync(foundApp, syncLicenseRequest.LicenseData, true)
 	if err != nil {
 		logger.Error(err)
 		w.WriteHeader(500)
diff --git a/kotsadm/pkg/license/license.go b/kotsadm/pkg/license/license.go
index 8dad4b766..f18ca9b52 100644
--- a/kotsadm/pkg/license/license.go
+++ b/kotsadm/pkg/license/license.go
@@ -10,9 +10,11 @@ import (
 	"github.com/pkg/errors"
 	"github.com/replicatedhq/kots/kotsadm/pkg/app"
 	"github.com/replicatedhq/kots/kotsadm/pkg/kotsutil"
+	"github.com/replicatedhq/kots/kotsadm/pkg/logger"
 	"github.com/replicatedhq/kots/kotsadm/pkg/persistence"
 	"github.com/replicatedhq/kots/kotsadm/pkg/preflight"
 	"github.com/replicatedhq/kots/kotsadm/pkg/registry"
+	registrytypes "github.com/replicatedhq/kots/kotsadm/pkg/registry/types"
 	"github.com/replicatedhq/kots/kotsadm/pkg/render"
 	"github.com/replicatedhq/kots/kotsadm/pkg/version"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
@@ -22,7 +24,7 @@ import (
 	"k8s.io/client-go/kubernetes/scheme"
 )
 
-func Sync(a *app.App, licenseData string) (*kotsv1beta1.License, error) {
+func Sync(a *app.App, licenseData string, failOnVersionCreate bool) (*kotsv1beta1.License, error) {
 	archiveDir, err := version.GetAppVersionArchive(a.ID, a.CurrentSequence)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to get latest app version")
@@ -88,21 +90,34 @@ func Sync(a *app.App, licenseData string) (*kotsv1beta1.License, error) {
 			return nil, errors.Wrap(err, "failed to get registry settings for app")
 		}
 
-		if err := render.RenderDir(archiveDir, a.ID, appSequence, registrySettings); err != nil {
-			return nil, errors.Wrap(err, "failed to render new version")
+		if err := createNewVersion(a, archiveDir, appSequence, registrySettings); err != nil {
+			// ignore error here to prevent a failure to render the current version
+			// preventing the end-user from updating the application
+			if failOnVersionCreate {
+				return nil, err
+			}
+			logger.Errorf("Failed to create new version from license sync: %v", err)
 		}
+	}
 
-		newSequence, err := version.CreateVersion(a.ID, archiveDir, "License Change", a.CurrentSequence)
-		if err != nil {
-			return nil, errors.Wrap(err, "failed to create new version")
-		}
+	return latestLicense, nil
+}
 
-		if err := preflight.Run(a.ID, newSequence, archiveDir); err != nil {
-			return nil, errors.Wrap(err, "failed to run preflights")
-		}
+func createNewVersion(a *app.App, archiveDir string, appSequence int64, registrySettings *registrytypes.RegistrySettings) error {
+	if err := render.RenderDir(archiveDir, a.ID, appSequence, registrySettings); err != nil {
+		return errors.Wrap(err, "failed to render new version")
 	}
 
-	return latestLicense, nil
+	newSequence, err := version.CreateVersion(a.ID, archiveDir, "License Change", a.CurrentSequence)
+	if err != nil {
+		return errors.Wrap(err, "failed to create new version")
+	}
+
+	if err := preflight.Run(a.ID, newSequence, archiveDir); err != nil {
+		return errors.Wrap(err, "failed to run preflights")
+	}
+
+	return nil
 }
 
 func GetCurrentLicenseString(a *app.App) (string, error) {
diff --git a/kotsadm/pkg/updatechecker/updatechecker.go b/kotsadm/pkg/updatechecker/updatechecker.go
index 892694ea3..02aa2ee10 100644
--- a/kotsadm/pkg/updatechecker/updatechecker.go
+++ b/kotsadm/pkg/updatechecker/updatechecker.go
@@ -163,7 +163,7 @@ func CheckForUpdates(appID string, deploy bool) (int64, error) {
 	}
 
 	// sync license, this method is only called when online
-	_, err = license.Sync(a, "")
+	_, err = license.Sync(a, "", false)
 	if err != nil {
 		return 0, errors.Wrap(err, "failed to sync license")
 	}
diff --git a/kotsadm/web/package.json b/kotsadm/web/package.json
index 58342dab8..b574a6133 100644
--- a/kotsadm/web/package.json
+++ b/kotsadm/web/package.json
@@ -108,7 +108,7 @@
     "@bugsnag/plugin-react": "^6.2.0",
     "@grafana/ui": "^6.4.3",
     "@maji/react-prism": "^1.0.1",
-    "@replicatedhq/ship-init": "1.6.22",
+    "@replicatedhq/ship-init": "1.6.23",
     "@types/file-saver": "^2.0.1",
     "apexcharts": "3.19.3",
     "apollo-cache-inmemory": "1.6.6",
diff --git a/kotsadm/web/yarn.lock b/kotsadm/web/yarn.lock
index f1dc2a7de..1507f1224 100644
--- a/kotsadm/web/yarn.lock
+++ b/kotsadm/web/yarn.lock
@@ -1955,10 +1955,10 @@
     pkginfo "^0.4.1"
     popsicle "^9.2.0"
 
-"@replicatedhq/ship-init@1.6.22":
-  version "1.6.22"
-  resolved "https://registry.yarnpkg.com/@replicatedhq/ship-init/-/ship-init-1.6.22.tgz#1ac40632b546629a4e8f6b9ccda0cc726be4ead1"
-  integrity sha512-fjJ3bqGgOZm2paxpfaZb7Yh51LpNo6xRIB7ej/KYXMkSNB/pjLmFAYfizYsUGoenDGwvJELXU7KQanjLlJx5Eg==
+"@replicatedhq/ship-init@1.6.23":
+  version "1.6.23"
+  resolved "https://registry.yarnpkg.com/@replicatedhq/ship-init/-/ship-init-1.6.23.tgz#1fbd58c07cecffe80a60c803b2e4f859e6c5b947"
+  integrity sha512-HY1gmDutxcqqEKGbEc4JtBnwUKEMPRqxaxgwTJVF/SJwN7Qm7mOJBvwV76ZFKHxX2Mb/XXav7bZKfDMYq4MYWA==
   dependencies:
     brace "^0.11.1"
     classnames "^2.2.6"
diff --git a/pkg/template/depgraph.go b/pkg/template/depgraph.go
index 1d6bf5516..fe43ab147 100644
--- a/pkg/template/depgraph.go
+++ b/pkg/template/depgraph.go
@@ -12,6 +12,8 @@ import (
 
 type depGraph struct {
 	Dependencies map[string]map[string]struct{}
+	CertItems    map[string]map[string]struct{} // items that use the TLSCert template function. map of certnames to configoptions that provide the certname
+	KeyItems     map[string]map[string]struct{} // items that use the TLSKey template function. map of configoptions to the certnames they use
 }
 
 // these config functions are used to add their dependencies to the depGraph
@@ -21,12 +23,24 @@ func (d *depGraph) funcMap(parent string) template.FuncMap {
 		return dep
 	}
 
+	addCertFunc := func(certName string, _ ...string) string {
+		d.AddCert(parent, certName)
+		return certName
+	}
+
+	addKeyFunc := func(certName string, _ ...string) string {
+		d.AddKey(parent, certName)
+		return certName
+	}
+
 	return template.FuncMap{
 		"ConfigOption":          addDepFunc,
 		"ConfigOptionIndex":     addDepFunc,
 		"ConfigOptionData":      addDepFunc,
 		"ConfigOptionEquals":    addDepFunc,
 		"ConfigOptionNotEquals": addDepFunc,
+		"TLSCert":               addCertFunc,
+		"TLSKey":                addKeyFunc,
 	}
 }
 
@@ -46,6 +60,40 @@ func (d *depGraph) AddDep(source, newDependency string) {
 	d.Dependencies[source][newDependency] = struct{}{}
 }
 
+func (d *depGraph) AddCert(source, certName string) {
+	if d.CertItems == nil {
+		d.CertItems = make(map[string]map[string]struct{})
+	}
+	if _, ok := d.CertItems[certName]; !ok {
+		d.CertItems[certName] = make(map[string]struct{})
+	}
+
+	d.CertItems[certName][source] = struct{}{}
+}
+
+func (d *depGraph) AddKey(source, certName string) {
+	if d.KeyItems == nil {
+		d.KeyItems = make(map[string]map[string]struct{})
+	}
+	if _, ok := d.KeyItems[source]; !ok {
+		d.KeyItems[source] = make(map[string]struct{})
+	}
+
+	d.KeyItems[source][certName] = struct{}{}
+}
+
+func (d *depGraph) resolveCertKeys() {
+	for source, certNameMap := range d.KeyItems {
+		for certName := range certNameMap {
+			for certProvider := range d.CertItems[certName] {
+				if certProvider != source {
+					d.AddDep(source, certProvider)
+				}
+			}
+		}
+	}
+}
+
 func (d *depGraph) ResolveDep(resolvedDependency string) {
 	for _, depMap := range d.Dependencies {
 		delete(depMap, resolvedDependency)
@@ -66,7 +114,7 @@ func (d *depGraph) GetHeadNodes() ([]string, error) {
 		waitList := []string{}
 		for k, v := range d.Dependencies {
 			depsList := []string{}
-			for dep, _ := range v {
+			for dep := range v {
 				depsList = append(depsList, fmt.Sprintf("%q", dep))
 			}
 			waitItem := fmt.Sprintf(`%q depends on %s`, k, strings.Join(depsList, `, `))
@@ -87,31 +135,28 @@ func (d *depGraph) Copy() (depGraph, error) {
 	var buf bytes.Buffer
 	enc := json.NewEncoder(&buf)
 	dec := json.NewDecoder(&buf)
-	err := enc.Encode(d.Dependencies)
+	err := enc.Encode(d)
 	if err != nil {
 		return depGraph{}, err
 	}
-	var depCopy map[string]map[string]struct{}
+	depCopy := depGraph{}
 	err = dec.Decode(&depCopy)
 	if err != nil {
 		return depGraph{}, err
 	}
 
-	return depGraph{
-		Dependencies: depCopy,
-	}, nil
+	return depCopy, nil
 
 }
 
 func (d *depGraph) ParseConfigGroup(configGroups []kotsv1beta1.ConfigGroup) error {
-	staticCtx := &StaticCtx{}
 	for _, configGroup := range configGroups {
 		for _, configItem := range configGroup.Items {
 			// add this to the dependency graph
 			d.AddNode(configItem.Name)
 
 			depBuilder := Builder{
-				Ctx:    []Ctx{staticCtx},
+				Ctx:    []Ctx{},
 				Functs: d.funcMap(configItem.Name),
 			}
 
@@ -122,5 +167,7 @@ func (d *depGraph) ParseConfigGroup(configGroups []kotsv1beta1.ConfigGroup) erro
 		}
 	}
 
+	d.resolveCertKeys()
+
 	return nil
 }


Test patch:
diff --git a/pkg/template/depgraph_test.go b/pkg/template/depgraph_test.go
index 13ea204a8..7e66771b8 100644
--- a/pkg/template/depgraph_test.go
+++ b/pkg/template/depgraph_test.go
@@ -12,6 +12,8 @@ import (
 
 type depGraphTestCase struct {
 	dependencies   map[string][]string
+	testCerts      map[string][]string
+	testKeys       map[string][]string
 	resolveOrder   []string
 	expectError    bool   //expect an error fetching head nodes
 	expectNotFound string //expect this dependency not to be part of the head nodes
@@ -113,6 +115,41 @@ func TestDepGraph(t *testing.T) {
 			expectError:  true,
 			name:         "does_not_exist",
 		},
+		{
+			dependencies: map[string][]string{
+				"alpha":   {},
+				"bravo":   {"alpha"},
+				"charlie": {"bravo"},
+				"delta":   {"alpha", "charlie"},
+				"echo":    {},
+			},
+			testCerts: map[string][]string{
+				"echo": {"certA"},
+			},
+			testKeys: map[string][]string{
+				"delta": {"certA"},
+			},
+			resolveOrder: []string{"alpha", "bravo", "charlie", "echo", "delta"},
+			name:         "basic_certs",
+		},
+		{
+			dependencies: map[string][]string{
+				"alpha":   {},
+				"bravo":   {"alpha"},
+				"charlie": {"bravo"},
+				"delta":   {"alpha", "charlie"},
+				"echo":    {},
+			},
+			testCerts: map[string][]string{
+				"echo": {"certA"},
+			},
+			testKeys: map[string][]string{
+				"delta": {"certA"},
+			},
+			resolveOrder:   []string{"alpha", "bravo", "charlie", "delta", "echo"},
+			name:           "basic_certs_original_order",
+			expectNotFound: "delta",
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
@@ -126,6 +163,18 @@ func TestDepGraph(t *testing.T) {
 					graph.AddDep(source, dep)
 				}
 			}
+			for source, certNames := range test.testCerts {
+				for _, certName := range certNames {
+					graph.AddCert(source, certName)
+				}
+			}
+			for source, keys := range test.testKeys {
+				for _, key := range keys {
+					graph.AddKey(source, key)
+				}
+			}
+			graph.resolveCertKeys()
+
 			runGraphTests(t, test, graph)
 		})
 
@@ -135,7 +184,7 @@ func TestDepGraph(t *testing.T) {
 
 			graph := depGraph{}
 
-			groups := buildTestConfigGroups(test.dependencies, "templateStringStart", "templateStringEnd", true)
+			groups := buildTestConfigGroups(test.dependencies, test.testCerts, test.testKeys, "templateStringStart", "templateStringEnd", true)
 
 			err := graph.ParseConfigGroup(groups)
 			require.NoError(t, err)
@@ -146,7 +195,7 @@ func TestDepGraph(t *testing.T) {
 }
 
 // this makes sure that we test with each of the configOption types, in both Value and Default
-func buildTestConfigGroups(dependencies map[string][]string, prefix string, suffix string, rotate bool) []kotsv1beta1.ConfigGroup {
+func buildTestConfigGroups(dependencies, certs, keys map[string][]string, prefix string, suffix string, rotate bool) []kotsv1beta1.ConfigGroup {
 	group := kotsv1beta1.ConfigGroup{}
 	group.Items = make([]kotsv1beta1.ConfigItem, 0)
 	counter := 0
@@ -156,7 +205,7 @@ func buildTestConfigGroups(dependencies map[string][]string, prefix string, suff
 		"{{repl ConfigOptionIndex \"%s\" }}",
 		"{{repl ConfigOptionData \"%s\" }}",
 		"repl{{ ConfigOptionEquals \"%s\" \"abc\" }}",
-		"{{repl ConfigOptionNotEquals \"%s\" \"xyz\" }}{{repl DoesNotExistFunc }}",
+		"{{repl ConfigOptionNotEquals \"%s\" \"xyz\" }}",
 	}
 
 	if !rotate {
@@ -166,12 +215,28 @@ func buildTestConfigGroups(dependencies map[string][]string, prefix string, suff
 		}
 	}
 
+	totalDepItems := 0
+
 	for source, deps := range dependencies {
 		newItem := kotsv1beta1.ConfigItem{Type: "text", Name: source}
 		depString := prefix
-		for i, dep := range deps {
-			depString += fmt.Sprintf(templateFuncs[i%len(templateFuncs)], dep)
+		for _, dep := range deps {
+			depString += fmt.Sprintf(templateFuncs[totalDepItems%len(templateFuncs)], dep)
+			totalDepItems++
+		}
+
+		if certNames, ok := certs[source]; ok {
+			for _, certName := range certNames {
+				depString += fmt.Sprintf("{{repl TLSCert \"%s\" }}", certName)
+			}
 		}
+
+		if certNames, ok := keys[source]; ok {
+			for _, certName := range certNames {
+				depString += fmt.Sprintf("{{repl TLSKey \"%s\" }}", certName)
+			}
+		}
+
 		depString += suffix
 
 		if counter%2 == 0 {