Repository: replicatedhq/kots
Pull request #912 (2020-08-06 21:50:16)

Grayson fix empty state flash


Patch:
diff --git a/kotsadm/pkg/handlers/snapshots.go b/kotsadm/pkg/handlers/snapshots.go
index 084dae581..b2e69bf35 100644
--- a/kotsadm/pkg/handlers/snapshots.go
+++ b/kotsadm/pkg/handlers/snapshots.go
@@ -230,6 +230,7 @@ func UpdateGlobalSnapshotSettings(w http.ResponseWriter, r *http.Request) {
 		store.Azure = nil
 		store.Internal = nil
 
+		store.Provider = "aws"
 		if updateGlobalSnapshotSettingsRequest.Other.AccessKeyID != "" {
 			store.Other.AccessKeyID = updateGlobalSnapshotSettingsRequest.Other.AccessKeyID
 		}
diff --git a/kotsadm/pkg/handlers/update.go b/kotsadm/pkg/handlers/update.go
index 24bdbe5b7..f2e3caeef 100644
--- a/kotsadm/pkg/handlers/update.go
+++ b/kotsadm/pkg/handlers/update.go
@@ -17,7 +17,8 @@ type AppUpdateCheckRequest struct {
 }
 
 type AppUpdateCheckResponse struct {
-	AvailableUpdates int64 `json:"availableUpdates"`
+	AvailableUpdates   int64 `json:"availableUpdates"`
+	CurrentAppSequence int64 `json:"currentAppSequence"`
 }
 
 func AppUpdateCheck(w http.ResponseWriter, r *http.Request) {
@@ -67,7 +68,8 @@ func AppUpdateCheck(w http.ResponseWriter, r *http.Request) {
 	}
 
 	appUpdateCheckResponse := AppUpdateCheckResponse{
-		AvailableUpdates: availableUpdates,
+		AvailableUpdates:   availableUpdates,
+		CurrentAppSequence: foundApp.CurrentSequence,
 	}
 
 	JSON(w, 200, appUpdateCheckResponse)
diff --git a/kotsadm/pkg/snapshot/store.go b/kotsadm/pkg/snapshot/store.go
index 6c9af3688..241647a4e 100644
--- a/kotsadm/pkg/snapshot/store.go
+++ b/kotsadm/pkg/snapshot/store.go
@@ -152,11 +152,68 @@ func UpdateGlobalStore(store *types.Store) (*velerov1.BackupStorageLocation, err
 		}
 	} else if store.Other != nil {
 		kotsadmVeleroBackendStorageLocation.Spec.Config = map[string]string{
-			"region": store.Other.Region,
-			"s3Url":  store.Other.Endpoint,
+			"region":           store.Other.Region,
+			"s3Url":            store.Other.Endpoint,
+			"s3ForcePathStyle": "true",
+		}
+
+		otherCfg := ini.Empty()
+		section, err := otherCfg.NewSection("default")
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to create default section in other creds")
+		}
+		_, err = section.NewKey("aws_access_key_id", store.Other.AccessKeyID)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to create other access key id")
+		}
+
+		_, err = section.NewKey("aws_secret_access_key", store.Other.SecretAccessKey)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to create other secret access key")
+		}
+
+		var otherCredentials bytes.Buffer
+		writer := bufio.NewWriter(&otherCredentials)
+		_, err = otherCfg.WriteTo(writer)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to write ini")
+		}
+		if err := writer.Flush(); err != nil {
+			return nil, errors.Wrap(err, "failed to flush buffer")
 		}
 
 		// create or update the secret
+		if kuberneteserrors.IsNotFound(currentSecretErr) {
+			// create
+			toCreate := corev1.Secret{
+				TypeMeta: metav1.TypeMeta{
+					APIVersion: "v1",
+					Kind:       "Secret",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "cloud-credentials",
+					Namespace: kotsadmVeleroBackendStorageLocation.Namespace,
+				},
+				Data: map[string][]byte{
+					"cloud": otherCredentials.Bytes(),
+				},
+			}
+			_, err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Create(context.TODO(), &toCreate, metav1.CreateOptions{})
+			if err != nil {
+				return nil, errors.Wrap(err, "failed to create other secret")
+			}
+		} else {
+			// update
+			if currentSecret.Data == nil {
+				currentSecret.Data = map[string][]byte{}
+			}
+
+			currentSecret.Data["cloud"] = otherCredentials.Bytes()
+			_, err = clientset.CoreV1().Secrets(kotsadmVeleroBackendStorageLocation.Namespace).Update(context.TODO(), currentSecret, metav1.UpdateOptions{})
+			if err != nil {
+				return nil, errors.Wrap(err, "failed to update other secret")
+			}
+		}
 	} else if store.Internal != nil {
 		kotsadmVeleroBackendStorageLocation.Spec.Config = map[string]string{
 			"region":           store.Internal.Region,
diff --git a/kotsadm/web/src/components/apps/AppSnapshots.jsx b/kotsadm/web/src/components/apps/AppSnapshots.jsx
index 22ebc8d8e..79f4db0f2 100644
--- a/kotsadm/web/src/components/apps/AppSnapshots.jsx
+++ b/kotsadm/web/src/components/apps/AppSnapshots.jsx
@@ -368,7 +368,7 @@ class AppSnapshots extends Component {
     const appTitle = app?.name;
     const inProgressSnapshotExist = snapshots?.find(snapshot => snapshot.status === "InProgress");
 
-    if (isLoadingSnapshotSettings || (isStartButtonClicked && snapshots?.length === 0) || startingSnapshot) {
+    if (isLoadingSnapshotSettings || !hasSnapshotsLoaded || (isStartButtonClicked && snapshots?.length === 0) || startingSnapshot) {
       return (
         <div className="flex-column flex1 alignItems--center justifyContent--center">
           <Loader size="60" />
diff --git a/kotsadm/web/src/components/apps/AppVersionHistory.jsx b/kotsadm/web/src/components/apps/AppVersionHistory.jsx
index 4df847278..e732b1dc5 100644
--- a/kotsadm/web/src/components/apps/AppVersionHistory.jsx
+++ b/kotsadm/web/src/components/apps/AppVersionHistory.jsx
@@ -584,15 +584,21 @@ class AppVersionHistory extends Component {
         this.props.refreshAppData();
         const response = await res.json();
         if (response.availableUpdates === 0) {
-          this.setState({
-            checkingForUpdates: false,
-            noUpdateAvailiableText: "There are no updates available",
-          });
-          setTimeout(() => {
+          if (!find(this.props.data?.getKotsDownstreamHistory, { parentSequence: response.currentAppSequence })) {
+            // version history list is out of sync - most probably because of automatic updates happening in the background - refetch list
+            this.props.data?.refetch();
+            this.setState({ checkingForUpdates: false });
+          } else {
             this.setState({
-              noUpdateAvailiableText: null,
+              checkingForUpdates: false,
+              noUpdateAvailiableText: "There are no updates available",
             });
-          }, 3000);
+            setTimeout(() => {
+              this.setState({
+                noUpdateAvailiableText: null,
+              });
+            }, 3000);
+          }
         } else {
           this.state.updateChecker.start(this.updateStatus, 1000);
         }
diff --git a/pkg/template/depgraph.go b/pkg/template/depgraph.go
index fe43ab147..4f662c1c4 100644
--- a/pkg/template/depgraph.go
+++ b/pkg/template/depgraph.go
@@ -11,9 +11,14 @@ 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
+	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 TLSKey configoptions to the certnames they use
+	CAItems             map[string]map[string]struct{} // items that use the TLSCACert template function. map of canames to configoptions that provide the caname
+	CAFromCertItems     map[string]map[string]struct{} // items that use the TLSCertFromCA template function. map of TLSCertFromCA configoptions to the canames they use
+	CertFromCACertItems map[string]map[string]struct{} // items that use the TLSCertFromCA template function. map of canames and certnames to configoptions that provide the certname
+	CAItemsFromKey      map[string]map[string]struct{} // items that use the TLSKeyFromCA template function. map of TLSKeyFromCA configoptions to the canames they use
+	CACertItemsFromKey  map[string]map[string]struct{} // items that use the TLSKeyFromCA template function. map of TLSKeyFromCA configoptions to the canames and certnames they use
 }
 
 // these config functions are used to add their dependencies to the depGraph
@@ -33,14 +38,32 @@ func (d *depGraph) funcMap(parent string) template.FuncMap {
 		return certName
 	}
 
+	addCAFunc := func(caName string, _ ...string) string {
+		d.AddCA(parent, caName)
+		return caName
+	}
+
+	addCertFromCAFunc := func(caName, certName string, _ ...string) string {
+		d.AddCertFromCA(parent, caName, certName)
+		return certName
+	}
+
+	addKeyFromCAFunc := func(caName, certName string, _ ...string) string {
+		d.AddKeyFromCA(parent, caName, certName)
+		return certName
+	}
+
 	return template.FuncMap{
 		"ConfigOption":          addDepFunc,
 		"ConfigOptionIndex":     addDepFunc,
 		"ConfigOptionData":      addDepFunc,
 		"ConfigOptionEquals":    addDepFunc,
 		"ConfigOptionNotEquals": addDepFunc,
+		"TLSCACert":             addCAFunc,
 		"TLSCert":               addCertFunc,
+		"TLSCertFromCA":         addCertFromCAFunc,
 		"TLSKey":                addKeyFunc,
+		"TLSKeyFromCA":          addKeyFromCAFunc,
 	}
 }
 
@@ -61,25 +84,37 @@ func (d *depGraph) AddDep(source, newDependency string) {
 }
 
 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{}{}
+	d.CertItems = addDepGraphItem(d.CertItems, certName, source)
 }
 
 func (d *depGraph) AddKey(source, certName string) {
-	if d.KeyItems == nil {
-		d.KeyItems = make(map[string]map[string]struct{})
+	d.KeyItems = addDepGraphItem(d.KeyItems, source, certName)
+}
+
+func (d *depGraph) AddCA(source, caName string) {
+	d.CAItems = addDepGraphItem(d.CAItems, caName, source)
+}
+
+func (d *depGraph) AddCertFromCA(source, caName, certName string) {
+	d.CAFromCertItems = addDepGraphItem(d.CAFromCertItems, source, caName)
+	d.CertFromCACertItems = addDepGraphItem(d.CertFromCACertItems, caName+certName, source)
+}
+
+func (d *depGraph) AddKeyFromCA(source, caName, certName string) {
+	d.CAItemsFromKey = addDepGraphItem(d.CAItemsFromKey, source, caName)
+	d.CACertItemsFromKey = addDepGraphItem(d.CACertItemsFromKey, source, caName+certName)
+}
+
+func addDepGraphItem(m map[string]map[string]struct{}, key, value string) map[string]map[string]struct{} {
+	if m == nil {
+		m = make(map[string]map[string]struct{})
 	}
-	if _, ok := d.KeyItems[source]; !ok {
-		d.KeyItems[source] = make(map[string]struct{})
+	if _, ok := m[key]; !ok {
+		m[key] = make(map[string]struct{})
 	}
 
-	d.KeyItems[source][certName] = struct{}{}
+	m[key][value] = struct{}{}
+	return m
 }
 
 func (d *depGraph) resolveCertKeys() {
@@ -94,6 +129,39 @@ func (d *depGraph) resolveCertKeys() {
 	}
 }
 
+func (d *depGraph) resolveCACerts() {
+	for source, caNameMap := range d.CAFromCertItems {
+		for caName := range caNameMap {
+			for caProvider := range d.CAItems[caName] {
+				if caProvider != source {
+					d.AddDep(source, caProvider)
+				}
+			}
+		}
+	}
+}
+
+func (d *depGraph) resolveCACertKeys() {
+	for source, caNameMap := range d.CAItemsFromKey {
+		for caName := range caNameMap {
+			for caProvider := range d.CAItems[caName] {
+				if caProvider != source {
+					d.AddDep(source, caProvider)
+				}
+			}
+		}
+	}
+	for source, caCertNameMap := range d.CACertItemsFromKey {
+		for caCertName := range caCertNameMap {
+			for certProvider := range d.CertFromCACertItems[caCertName] {
+				if certProvider != source {
+					d.AddDep(source, certProvider)
+				}
+			}
+		}
+	}
+}
+
 func (d *depGraph) ResolveDep(resolvedDependency string) {
 	for _, depMap := range d.Dependencies {
 		delete(depMap, resolvedDependency)
@@ -168,6 +236,7 @@ func (d *depGraph) ParseConfigGroup(configGroups []kotsv1beta1.ConfigGroup) erro
 	}
 
 	d.resolveCertKeys()
+	d.resolveCACertKeys()
 
 	return nil
 }


Test patch:
diff --git a/pkg/template/depgraph_test.go b/pkg/template/depgraph_test.go
index 7e66771b8..ff2e3fb40 100644
--- a/pkg/template/depgraph_test.go
+++ b/pkg/template/depgraph_test.go
@@ -11,12 +11,15 @@ 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
+	dependencies    map[string][]string
+	testCerts       map[string][]string
+	testKeys        map[string][]string
+	testCAs         map[string][]string
+	testCAFromCerts map[string][][2]string
+	testCAFromKeys  map[string][][2]string
+	resolveOrder    []string
+	expectError     bool     //expect an error fetching head nodes
+	expectNotFound  []string //expect these dependencies not to be part of the head nodes
 
 	name string
 }
@@ -64,7 +67,7 @@ func TestDepGraph(t *testing.T) {
 				"foxtrot": {},
 			},
 			resolveOrder:   []string{"alpha", "bravo", "charlie", "delta", "echo", "foxtrot"},
-			expectNotFound: "delta",
+			expectNotFound: []string{"delta"},
 			name:           "unresolved_dependency",
 		},
 		{
@@ -148,7 +151,83 @@ func TestDepGraph(t *testing.T) {
 			},
 			resolveOrder:   []string{"alpha", "bravo", "charlie", "delta", "echo"},
 			name:           "basic_certs_original_order",
-			expectNotFound: "delta",
+			expectNotFound: []string{"delta"},
+		},
+		{
+			dependencies: map[string][]string{
+				"alpha":   {},
+				"bravo":   {"alpha"},
+				"charlie": {"alpha", "bravo"},
+				"delta":   {},
+				"echo":    {},
+			},
+			testCAs: map[string][]string{
+				"echo": {"caA"},
+			},
+			testCAFromCerts: map[string][][2]string{
+				"delta": {{"caA", "certA"}},
+			},
+			testCAFromKeys: map[string][][2]string{
+				"charlie": {{"caA", "certA"}},
+			},
+			resolveOrder: []string{"alpha", "bravo", "echo", "delta", "charlie"},
+			name:         "basic_cacerts",
+		},
+		{
+			dependencies: map[string][]string{
+				"alpha":   {},
+				"bravo":   {"alpha"},
+				"charlie": {"alpha", "bravo"},
+				"delta":   {},
+				"echo":    {},
+			},
+			testCAs: map[string][]string{
+				"echo": {"caA"},
+			},
+			testCAFromCerts: map[string][][2]string{
+				"delta": {{"caA", "certA"}},
+			},
+			testCAFromKeys: map[string][][2]string{
+				"charlie": {{"caA", "certA"}},
+			},
+			resolveOrder:   []string{"alpha", "bravo", "charlie", "delta", "echo"},
+			name:           "basic_cacerts_original_order",
+			expectNotFound: []string{"delta", "charlie"},
+		},
+		{
+			dependencies: map[string][]string{
+				"alpha":   {},
+				"bravo":   {"alpha"},
+				"charlie": {"bravo"},
+				"delta":   {"alpha", "charlie"},
+				"echo":    {},
+			},
+			testCAs: map[string][]string{
+				"echo": {"caA"},
+			},
+			testCAFromKeys: map[string][][2]string{
+				"delta": {{"caA", "certA"}},
+			},
+			resolveOrder: []string{"alpha", "bravo", "charlie", "echo", "delta"},
+			name:         "basic_cacerts_key_only",
+		},
+		{
+			dependencies: map[string][]string{
+				"alpha":   {},
+				"bravo":   {"alpha"},
+				"charlie": {"bravo"},
+				"delta":   {"alpha", "charlie"},
+				"echo":    {},
+			},
+			testCAs: map[string][]string{
+				"echo": {"caA"},
+			},
+			testCAFromKeys: map[string][][2]string{
+				"delta": {{"caA", "certA"}},
+			},
+			resolveOrder:   []string{"alpha", "bravo", "charlie", "delta", "echo"},
+			name:           "basic_cacerts_key_only_original_order",
+			expectNotFound: []string{"delta"},
 		},
 	}
 	for _, test := range tests {
@@ -173,7 +252,24 @@ func TestDepGraph(t *testing.T) {
 					graph.AddKey(source, key)
 				}
 			}
+			for source, caNames := range test.testCAs {
+				for _, caName := range caNames {
+					graph.AddCA(source, caName)
+				}
+			}
+			for source, caCertNames := range test.testCAFromCerts {
+				for _, caCertName := range caCertNames {
+					graph.AddCertFromCA(source, caCertName[0], caCertName[1])
+				}
+			}
+			for source, caCertNames := range test.testCAFromKeys {
+				for _, caCertName := range caCertNames {
+					graph.AddKeyFromCA(source, caCertName[0], caCertName[1])
+				}
+			}
 			graph.resolveCertKeys()
+			graph.resolveCACerts()
+			graph.resolveCACertKeys()
 
 			runGraphTests(t, test, graph)
 		})
@@ -184,7 +280,11 @@ func TestDepGraph(t *testing.T) {
 
 			graph := depGraph{}
 
-			groups := buildTestConfigGroups(test.dependencies, test.testCerts, test.testKeys, "templateStringStart", "templateStringEnd", true)
+			groups := buildTestConfigGroups(
+				test.dependencies, test.testCerts, test.testKeys,
+				test.testCAs, test.testCAFromCerts, test.testCAFromKeys,
+				"templateStringStart", "templateStringEnd", true,
+			)
 
 			err := graph.ParseConfigGroup(groups)
 			require.NoError(t, err)
@@ -195,7 +295,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, certs, keys map[string][]string, prefix string, suffix string, rotate bool) []kotsv1beta1.ConfigGroup {
+func buildTestConfigGroups(dependencies, certs, keys, cas map[string][]string, caFromCerts, caFromKeys map[string][][2]string, prefix string, suffix string, rotate bool) []kotsv1beta1.ConfigGroup {
 	group := kotsv1beta1.ConfigGroup{}
 	group.Items = make([]kotsv1beta1.ConfigItem, 0)
 	counter := 0
@@ -237,6 +337,24 @@ func buildTestConfigGroups(dependencies, certs, keys map[string][]string, prefix
 			}
 		}
 
+		if caNames, ok := cas[source]; ok {
+			for _, caName := range caNames {
+				depString += fmt.Sprintf("{{repl TLSCACert \"%s\" }}", caName)
+			}
+		}
+
+		if caCertNames, ok := caFromCerts[source]; ok {
+			for _, caCertName := range caCertNames {
+				depString += fmt.Sprintf("{{repl TLSCertFromCA \"%s\" \"%s\" }}", caCertName[0], caCertName[1])
+			}
+		}
+
+		if caCertNames, ok := caFromKeys[source]; ok {
+			for _, caCertName := range caCertNames {
+				depString += fmt.Sprintf("{{repl TLSKeyFromCA \"%s\" \"%s\" }}", caCertName[0], caCertName[1])
+			}
+		}
+
 		depString += suffix
 
 		if counter%2 == 0 {
@@ -268,7 +386,7 @@ func runGraphTests(t *testing.T, test depGraphTestCase, graph depGraph) {
 
 		require.NoError(t, err, "toResolve: %s", toResolve)
 
-		if test.expectNotFound != "" && toResolve == test.expectNotFound {
+		if stringInSlice(toResolve, test.expectNotFound) {
 			require.NotContains(t, available, toResolve)
 			return
 		}
@@ -286,3 +404,12 @@ func runGraphTests(t *testing.T, test depGraphTestCase, graph depGraph) {
 
 	require.Equal(t, depLen, len(graphCopy.Dependencies))
 }
+
+func stringInSlice(a string, list []string) bool {
+	for _, b := range list {
+		if b == a {
+			return true
+		}
+	}
+	return false
+}