Repository: iter8-tools/iter8
Pull request #1339 (2022-09-19 18:38:21)

Reformat chart group config


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 79f7601e..90214acf 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -17,6 +17,7 @@ linters:
   - govet
   - staticcheck
   - gosec
+  - misspell
 
 issues:
   exclude-use-default: false
diff --git a/abn/application/applicationmap.go b/abn/application/applicationmap.go
index 7fb48365..ba3a09aa 100644
--- a/abn/application/applicationmap.go
+++ b/abn/application/applicationmap.go
@@ -35,12 +35,12 @@ var (
 	Applications ThreadSafeApplicationMap
 	// BatchWriteInterval is the interval over which changes may batched before being persisted
 	BatchWriteInterval time.Duration
-	// maxApplicationDataBytes is the maximum number of bytes allowed in an applicaton (as YAML converted to []byte)
+	// maxApplicationDataBytes is the maximum number of bytes allowed in an application (as YAML converted to []byte)
 	// this limit prevents trying to persist an application that is too large (Kubernetes secrets have a 1 MB size limit)
 	maxApplicationDataBytes int
 )
 
-// initalize global variables
+// initialize global variables
 func init() {
 	Applications = ThreadSafeApplicationMap{
 		apps:           map[string]*Application{},
diff --git a/abn/k8sclient/k8sclient.go b/abn/k8sclient/k8sclient.go
index 5d7ef05c..2adad8d0 100644
--- a/abn/k8sclient/k8sclient.go
+++ b/abn/k8sclient/k8sclient.go
@@ -13,7 +13,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 )
 
-// Client is a global variable. Before use, it must be assigned and initalized
+// Client is a global variable. Before use, it must be assigned and initialized
 var Client KubeClient
 
 // KubeClient embeds Kube configuration, and
diff --git a/autox/config.go b/autox/config.go
index 657c33b8..7275ee48 100644
--- a/autox/config.go
+++ b/autox/config.go
@@ -15,31 +15,33 @@ import (
 // resourceConfig is the configuration that identifies the resources to watch and in which namespaces.
 type resourceConfig struct {
 	// Namespaces is list of namespaces to watch
-	Namespaces []string `yaml:"namespaces,omitempty"`
+	Namespaces []string `json:"namespaces,omitempty" yaml:"namespaces,omitempty"`
 
 	// Resources is list of resoure types that should be watched in the namespaces
-	Resources []schema.GroupVersionResource `yaml:"resources,omitempty"`
+	Resources []schema.GroupVersionResource `json:"resources,omitempty" yaml:"resources,omitempty"`
 }
 
 // chart points to a particular Helm chart
 type chart struct {
 	// Repo is the repo of the Helm chart
-	Repo string `yaml:"repo"`
+	Repo string `json:"repo" yaml:"repo"`
 
 	// Name is the name of the Helm chart
-	Name string `yaml:"name"`
+	Name string `json:"name" yaml:"name"`
 
 	// ValuesFileURL is the URL to the values file of the Helm chart
-	ValuesFileURL string `yaml:"valuesFileURL"`
+	ValuesFileURL string `json:"valuesFileURL" yaml:"valuesFileURL"`
 }
 
+// chartGroup is the configuration of all the Helm charts for a particular experiment group
+// the key is an identifier for the chart
 type chartGroup struct {
-	Name string `yaml:"name"`
-
-	Charts []chart `yaml:"charts"`
+	Charts map[string]chart `json:"charts" yaml:"charts"`
 }
 
-type chartGroupConfig []chartGroup
+// chartGroupConfig is the configuration for all the Helm charts
+// the key is the experiment group name
+type chartGroupConfig map[string]chartGroup
 
 // readResourceConfig reads yaml config file and converts to a resourceConfig object
 func readResourceConfig(fp string) (config resourceConfig) {
diff --git a/autox/informer.go b/autox/informer.go
index 60b4a57e..4f19a478 100644
--- a/autox/informer.go
+++ b/autox/informer.go
@@ -3,36 +3,146 @@ package autox
 // informer.go - informer(s) to watch desired resources/namespaces
 
 import (
+	"fmt"
+	"reflect"
+
 	"github.com/iter8-tools/iter8/base/log"
-	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/client-go/dynamic/dynamicinformer"
 	"k8s.io/client-go/tools/cache"
 )
 
+const (
+	autoxLabel = "iter8.tools/autox-group"
+)
+
+type chartAction int64
+
+const (
+	releaseAction chartAction = 0
+	deleteAction  chartAction = 1
+)
+
+func getReleaseName(chartGroupName string, chartName string) string {
+	return fmt.Sprintf("autox-%s-%s", chartGroupName, chartName)
+}
+
+func installHelmReleases(resourceName string) {
+	doChartAction(resourceName, releaseAction)
+}
+
+func installHelmRelease(releaseName string, chart chart) {
+	// TODO: check if there is a preexisting Helm release
+
+	// TODO: mutex
+
+	// TODO: release Helm chart
+
+	log.Logger.Debug("Release chart:", releaseName)
+}
+
+func deleteHelmReleases(resourceName string) {
+	doChartAction(resourceName, deleteAction)
+}
+
+func deleteHelmRelease(releaseName string, chart chart) {
+	// TODO: check if there is a preexisting Helm release
+
+	// TODO: mutex
+
+	// TODO: delete Helm chart
+
+	log.Logger.Debug("Delete chart:", releaseName)
+}
+
+func doChartAction(resourceName string, chartAction chartAction) {
+	if cg, ok := iter8ChartGroupConfig[resourceName]; ok {
+		for chartName, chart := range cg.Charts {
+			releaseName := getReleaseName(resourceName, chartName)
+
+			switch chartAction {
+			case releaseAction:
+				installHelmRelease(releaseName, chart)
+
+			case deleteAction:
+				deleteHelmRelease(releaseName, chart)
+			}
+		}
+	} else {
+		// TODO: what log level should this be?
+		log.Logger.Debug("AutoX should make a Helm release for app \"", resourceName, "\" but no Helm charts were provided in the chartGroupConfig")
+	}
+}
+
 var addObject = func(obj interface{}) {
 	log.Logger.Debug("Add:", obj)
+
+	uObj := obj.(*unstructured.Unstructured)
+	resourceName := uObj.GetName()
+	// example label: iter8.tools/autox-group=hello
+	labels := pruneLabels(uObj.GetLabels())
+
+	// check if the app name matches the name in the autox label
+	if autoxLabelName, ok := labels[autoxLabel]; ok && resourceName == autoxLabelName {
+		// Release Helm charts
+		installHelmReleases(resourceName)
+	}
+}
+
+func pruneLabels(labels map[string]string) map[string]string {
+	// TODO: select labels important for autoX
+
+	return labels
 }
 
 var updateObject = func(oldObj, obj interface{}) {
 	log.Logger.Debug("Update:", oldObj, obj)
+
+	uOldObj := oldObj.(*unstructured.Unstructured)
+	oldLabels := pruneLabels(uOldObj.GetLabels())
+
+	uObj := obj.(*unstructured.Unstructured)
+	resourceName := uObj.GetName()
+	// example label: iter8.tools/autox-group=hello
+	labels := pruneLabels(uObj.GetLabels())
+
+	// if the pruned labels are the same, do nothing
+	if reflect.DeepEqual(oldLabels, labels) {
+		return
+	}
+
+	// if the pruned labels are different, then update by deleting and reinstalling
+	deleteHelmReleases(resourceName)
+	installHelmReleases(resourceName)
 }
 
 var deleteObject = func(obj interface{}) {
 	log.Logger.Debug("Delete:", obj)
+
+	uObj := obj.(*unstructured.Unstructured)
+	resourceName := uObj.GetName()
+	// example label: iter8.tools/autox-group=hello
+	labels := pruneLabels(uObj.GetLabels())
+
+	// check if the app name matches the name in the autox label
+	if autoxLabelName, ok := labels[autoxLabel]; ok && resourceName == autoxLabelName {
+		// Delete Helm charts
+		deleteHelmReleases(resourceName)
+	}
 }
 
 type iter8Watcher struct {
 	factories map[string]dynamicinformer.DynamicSharedInformerFactory
 }
 
-func newIter8Watcher(resourceTypes []schema.GroupVersionResource, namespaces []string, groupConfig chartGroupConfig) *iter8Watcher {
+func newIter8Watcher() *iter8Watcher {
 	w := &iter8Watcher{
 		factories: map[string]dynamicinformer.DynamicSharedInformerFactory{},
 	}
 	// for each namespace, resource type configure Informer
-	for _, ns := range namespaces {
+	for _, ns := range iter8ResourceConfig.Namespaces {
 		w.factories[ns] = dynamicinformer.NewFilteredDynamicSharedInformerFactory(k8sClient.dynamicClient, 0, ns, nil)
-		for _, gvr := range resourceTypes {
+		for _, gvr := range iter8ResourceConfig.Resources {
 			informer := w.factories[ns].ForResource(gvr)
 			informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
 				AddFunc:    addObject,
diff --git a/autox/k8sclient.go b/autox/k8sclient.go
index 3cdb8545..e1261323 100644
--- a/autox/k8sclient.go
+++ b/autox/k8sclient.go
@@ -14,7 +14,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 )
 
-// k8sClient is a global variable. Before use, it must be assigned and initalized
+// k8sClient is a global variable. Before use, it must be assigned and initialized
 var k8sClient = newKubeClient(cli.New())
 
 // kubeClient embeds Kube configuration, and
diff --git a/autox/watcher.go b/autox/watcher.go
index 4e281f87..3e787109 100644
--- a/autox/watcher.go
+++ b/autox/watcher.go
@@ -16,6 +16,9 @@ const (
 	chartGroupConfigEnv = "CHART_GROUP_CONFIG"
 )
 
+var iter8ResourceConfig resourceConfig
+var iter8ChartGroupConfig chartGroupConfig
+
 // Start is entry point to configure services and start them
 func Start(stopCh chan struct{}) error {
 	// initialize kubernetes driver
@@ -36,12 +39,12 @@ func Start(stopCh chan struct{}) error {
 	}
 
 	// set up resource watching as defined by config
-	resourceConfig := readResourceConfig(resourceConfigFile)
-	chartGroupConfig := readChartGroupConfig(chartGroupConfigFile)
+	iter8ResourceConfig = readResourceConfig(resourceConfigFile)
+	iter8ChartGroupConfig = readChartGroupConfig(chartGroupConfigFile)
 
-	log.Logger.Debug("chartGroupConfig:", chartGroupConfig)
+	log.Logger.Debug("chartGroupConfig:", iter8ChartGroupConfig)
 
-	w := newIter8Watcher(resourceConfig.Resources, resourceConfig.Namespaces, chartGroupConfig)
+	w := newIter8Watcher()
 	go w.start(stopCh)
 	return nil
 }
diff --git a/base/notify.go b/base/notify.go
index 49f0763e..80834ebf 100644
--- a/base/notify.go
+++ b/base/notify.go
@@ -70,7 +70,7 @@ type Report struct {
 	Experiment *Experiment `json:"experiment" yaml:"experiment"`
 }
 
-// getReport gets the values for the payload tempalte
+// getReport gets the values for the payload template
 func getReport(exp *Experiment) map[string]Report {
 	return map[string]Report{
 		"Report": {


Test patch:
diff --git a/abn/core/grpc_service_test.go b/abn/core/grpc_service_test.go
index 3fa808f7..e06abc2d 100644
--- a/abn/core/grpc_service_test.go
+++ b/abn/core/grpc_service_test.go
@@ -33,9 +33,9 @@ type Scenario struct {
 
 func TestLookup(t *testing.T) {
 	testcases := map[string]Scenario{
-		"no applicaton": {application: "default/noapp", user: "foobar", errorSubstring: "application not found", track: ""},
-		"no user":       {application: "default/application", user: "", errorSubstring: "no user session provided", track: ""},
-		"valid":         {application: "default/application", user: "user", errorSubstring: "", track: "candidate"},
+		"no application": {application: "default/noapp", user: "foobar", errorSubstring: "application not found", track: ""},
+		"no user":        {application: "default/application", user: "", errorSubstring: "no user session provided", track: ""},
+		"valid":          {application: "default/application", user: "user", errorSubstring: "", track: "candidate"},
 	}
 
 	for label, scenario := range testcases {
@@ -92,10 +92,10 @@ func TestGetApplicationData(t *testing.T) {
 
 func TestWriteMetric(t *testing.T) {
 	testcases := map[string]Scenario{
-		"no applicaton": {application: "", user: "user", errorSubstring: "track not mapped", track: "", metric: "", value: "76"},
-		"no user":       {application: "default/application", user: "", errorSubstring: "no user session provided", track: "", metric: "", value: "76"},
-		"invalid value": {application: "default/application", user: "user", errorSubstring: "strconv.ParseFloat: parsing \"abc\": invalid syntax", track: "", metric: "", value: "abc"},
-		"valid":         {application: "default/application", user: "user", errorSubstring: "", track: "candidate", metric: "metric1", value: "76"},
+		"no application": {application: "", user: "user", errorSubstring: "track not mapped", track: "", metric: "", value: "76"},
+		"no user":        {application: "default/application", user: "", errorSubstring: "no user session provided", track: "", metric: "", value: "76"},
+		"invalid value":  {application: "default/application", user: "user", errorSubstring: "strconv.ParseFloat: parsing \"abc\": invalid syntax", track: "", metric: "", value: "abc"},
+		"valid":          {application: "default/application", user: "user", errorSubstring: "", track: "candidate", metric: "metric1", value: "76"},
 	}
 
 	for label, scenario := range testcases {
diff --git a/abn/watcher/informer_test.go b/abn/watcher/informer_test.go
index 5de0668e..34e5738f 100644
--- a/abn/watcher/informer_test.go
+++ b/abn/watcher/informer_test.go
@@ -166,7 +166,7 @@ func TestUpdate(t *testing.T) {
 						return r
 					}
 
-					// otherwise application was possibily modified in some way
+					// otherwise application was possibility modified in some way
 					// at least this application exists since it was preexisting
 					a, err := abnapp.Applications.Get(s.namespace + "/" + s.application)
 					r = r && assert.NoError(t, err)
@@ -276,7 +276,7 @@ func TestDelete(t *testing.T) {
 						return r
 					}
 
-					// otherwise application was possibily modified in some way
+					// otherwise application was possibility modified in some way
 					// at least this application exists since it was preexisting
 					a, err := abnapp.Applications.Get(s.namespace + "/" + s.application)
 					r = r && assert.NoError(t, err)
diff --git a/autox/config_test.go b/autox/config_test.go
index 0800e942..c66906ed 100644
--- a/autox/config_test.go
+++ b/autox/config_test.go
@@ -42,18 +42,18 @@ func TestReadGroupChartConfig(t *testing.T) {
 		numChartGroups int
 	}{
 		{"empty", "config.empty.yaml", 0},
-		{"invalid", "config.invalid.yaml", 0},
+		{"invalid", "config.invalid.yaml", 1},
 		{"garbage", "config.garbage.yaml", 0},
 		{"nofile", "config.nofile.yaml", 0},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
 			c := readChartGroupConfig(completePath("../testdata/autox_inputs", tt.file))
 			assert.Equal(t, tt.numChartGroups, len(c))
-
-			c = readChartGroupConfig(completePath("../testdata/autox_inputs", "group_config.example.yaml"))
-			assert.Equal(t, 2, len(c))
-			assert.Equal(t, 2, len(c[0].Charts))
-			assert.Equal(t, 1, len(c[1].Charts))
 		})
 	}
+
+	c := readChartGroupConfig(completePath("../testdata/autox_inputs", "group_config.example.yaml"))
+	assert.Equal(t, 2, len(c))
+	assert.Equal(t, 2, len(c["myApp"].Charts))
+	assert.Equal(t, 1, len(c["myApp2"].Charts))
 }
diff --git a/autox/informer_test.go b/autox/informer_test.go
index be8ffb3d..7869d2a4 100644
--- a/autox/informer_test.go
+++ b/autox/informer_test.go
@@ -26,7 +26,8 @@ const (
 )
 
 // Check to see if add, update, delete handlers from the watcher are properly invoked
-func TestWatcher(t *testing.T) {
+// after the watcher is created using newIter8Watcher()
+func TestNewIter8Watcher(t *testing.T) {
 	addObjectInvocations := 0
 	updateObjectInvocations := 0
 	deleteObjectInvocations := 0
@@ -36,10 +37,12 @@ func TestWatcher(t *testing.T) {
 		log.Logger.Debug("Add:", obj)
 		addObjectInvocations++
 	}
+
 	updateObject = func(oldObj, obj interface{}) {
 		log.Logger.Debug("Update:", oldObj, obj)
 		updateObjectInvocations++
 	}
+
 	deleteObject = func(obj interface{}) {
 		log.Logger.Debug("Delete:", obj)
 		deleteObjectInvocations++
@@ -57,11 +60,13 @@ func TestWatcher(t *testing.T) {
 
 	// define and start watcher
 	k8sClient = newFakeKubeClient(cli.New())
-	w := newIter8Watcher(
-		[]schema.GroupVersionResource{gvr},
-		[]string{namespace},
-		chartGroupConfig{},
-	)
+
+	iter8ResourceConfig = resourceConfig{
+		Namespaces: []string{namespace},
+		Resources:  []schema.GroupVersionResource{gvr},
+	}
+
+	w := newIter8Watcher()
 	assert.NotNil(t, w)
 	done := make(chan struct{})
 	defer close(done)
diff --git a/autox/watcher_test.go b/autox/watcher_test.go
index e87677f6..b847cb47 100644
--- a/autox/watcher_test.go
+++ b/autox/watcher_test.go
@@ -13,7 +13,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 )
 
-func TestAutoXStart(t *testing.T) {
+func TestStart(t *testing.T) {
 	addObjectInvocations := 0
 	addObject = func(obj interface{}) {
 		log.Logger.Debug("Add:", obj)
diff --git a/testdata/autox_inputs/group_config.example.yaml b/testdata/autox_inputs/group_config.example.yaml
index 72e409b0..fc22db8e 100644
--- a/testdata/autox_inputs/group_config.example.yaml
+++ b/testdata/autox_inputs/group_config.example.yaml
@@ -1,13 +1,16 @@
-- name: myApp
+myApp:
   charts:
-    - repo: abc
+    name1:
+      repo: abc
       name: def
       valuesFileUrl: abcdef.com/abcdef
-    - repo: ghi
+    name2:
+      repo: ghi
       name: jkl
       valuesFileUrl: ghijkl.com/ghijkl
-- name: myApp2
+myApp2:
   charts:
-    - repo: mno
+    name3: 
+      repo: mno
       name: pqr
       valuesFileUrl: mnopqr.com/mnopqr
\ No newline at end of file