Skip to content

Commit

Permalink
CASMINST-3664: Store loftsman ship log in its own configmap, and remo…
Browse files Browse the repository at this point in the history
…ve loftsman.io/previous-data annoation (#43)

* Stop populating the "loftsman.io/previous-data" annotation in the ship result configmap. Store the loftsman.log file in separate configmap.

* Add 1.2.0 changelog

* When creating the ship result configmap include the log configmap
annoation

* Regenerate interface mocks
  • Loading branch information
rsjostrand-hpe authored Dec 17, 2021
1 parent a781867 commit 04db488
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 138 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG/CHANGELOG-1.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### 1.2.0 Changelog

* Stop populating and using the `loftsman.io/previous-data` annotation when creating or updating the ship result configmap.
* Removed the loftsman log output from the ship result configmap.
* Added a new configmap to store the loftsman log output from a ship result into a separate configmap.
192 changes: 91 additions & 101 deletions docs/README.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion internal/interfaces/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ type Kubernetes interface {
IsRetryError(err error) bool
EnsureNamespace(name string) error
FindConfigMap(name string, namespace string, withKey string, withValue string) (*v1.ConfigMap, error)
InitializeConfigMap(name string, namespace string, data map[string]string) (*v1.ConfigMap, error)
InitializeShipConfigMap(name string, namespace string, data map[string]string) (*v1.ConfigMap, error)
InitializeLogConfigMap(name string, namespace string, data map[string]string) (*v1.ConfigMap, error)
PatchConfigMap(name string, namespace string, data map[string]string) (*v1.ConfigMap, error)
GetSecretKeyValue(secretName string, namespace string, dataKey string) (string, error)
}
63 changes: 51 additions & 12 deletions internal/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,17 +133,17 @@ func (k *Kubernetes) FindConfigMap(name string, namespace string, withKey string
return result, err
}

// InitializeConfigMap will ensure a configmap exists by name, in a namespace, with data. If an existing configmap
// is found, the previous configmap's data will be persisted to a an annotation on the new version of the configmap
func (k *Kubernetes) InitializeConfigMap(name string, namespace string, data map[string]string) (*v1.ConfigMap, error) {
// InitializeShipConfigMap will ensure a configmap exists by name, in a namespace, with data. If an existing configmap
// is found and it is presisting previous data, then remove any previous data in the new version of the configmap
func (k *Kubernetes) InitializeShipConfigMap(name string, namespace string, data map[string]string) (*v1.ConfigMap, error) {
var err error
var result *v1.ConfigMap
previousDataAnnotationKey := "loftsman.io/previous-data"
logConfigMapName := fmt.Sprintf("%s-ship-log", name)
err = retry.OnError(retry.DefaultBackoff, k.IsRetryError, func() error {
result, err = k.client.CoreV1().ConfigMaps(namespace).Get(context.Background(), name, metav1.GetOptions{})
if kerrors.IsNotFound(err) {
annotations := make(map[string]string)
annotations[previousDataAnnotationKey] = ""
annotations["loftsman.io/ship-log-configmap"] = logConfigMapName
result, err = k.client.CoreV1().ConfigMaps(namespace).Create(context.Background(), &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Expand All @@ -158,17 +158,56 @@ func (k *Kubernetes) InitializeConfigMap(name string, namespace string, data map
if err != nil {
return err
}
previousData, err := json.Marshal(result.Data)

// Remove legacy annoations and fields
patchData := map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"loftsman.io/previous-data": nil,
"loftsman.io/ship-log-configmap": logConfigMapName,
},
},
"data": map[string]interface{}{
"loftsman.log": nil,
},
}
patchDataEncoded, err := json.Marshal(patchData)
if err != nil {
return err
}
result.ObjectMeta.Annotations[previousDataAnnotationKey] = string(previousData)
patchData, err := json.Marshal(v1.ConfigMap{
ObjectMeta: result.ObjectMeta,
Data: data,
})

result, err = k.client.CoreV1().ConfigMaps(namespace).Patch(context.Background(), name,
types.StrategicMergePatchType, []byte(patchData), metav1.PatchOptions{})
types.MergePatchType, []byte(patchDataEncoded), metav1.PatchOptions{})

return err
})
return result, err
}

// InitializeLogConfigMap will ensure a configmap exists by name, in a namespace, with data. If an existing configmap
// is found then it will not be modifed
func (k *Kubernetes) InitializeLogConfigMap(name string, namespace string, data map[string]string) (*v1.ConfigMap, error) {
var err error
var result *v1.ConfigMap
err = retry.OnError(retry.DefaultBackoff, k.IsRetryError, func() error {
result, err = k.client.CoreV1().ConfigMaps(namespace).Get(context.Background(), name, metav1.GetOptions{})
if kerrors.IsNotFound(err) {
annotations := make(map[string]string)
result, err = k.client.CoreV1().ConfigMaps(namespace).Create(context.Background(), &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: k.getCommonLabels(),
Annotations: annotations,
},
Data: data,
}, metav1.CreateOptions{})
return err
}
if err != nil {
return err
}

return err
})
return result, err
Expand Down
40 changes: 36 additions & 4 deletions internal/kubernetes/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestFindConfigMapNotFound(t *testing.T) {
}
}

func TestInitializeConfigMapNew(t *testing.T) {
func TestInitializeShipConfigMapNew(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", `=~http://loftsman-tests`, httpmock.NewStringResponder(404, `{}`))
Expand All @@ -112,13 +112,13 @@ func TestInitializeConfigMapNew(t *testing.T) {
data := make(map[string]string)
data["one"] = "1"
data["two"] = "2"
_, err := k.InitializeConfigMap("loftsman-tests", "default", data)
_, err := k.InitializeShipConfigMap("loftsman-tests", "default", data)
if err != nil {
t.Errorf("Got unexpected error from kubernetes.TestInitializeConfigMapNew(): %s", err)
}
}

func TestInitializeConfigMapExists(t *testing.T) {
func TestInitializeShipConfigMapExists(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", `=~http://loftsman-tests`, httpmock.NewStringResponder(200, `{"metadata": {"annotations": {}}}`))
Expand All @@ -129,12 +129,44 @@ func TestInitializeConfigMapExists(t *testing.T) {
data := make(map[string]string)
data["one"] = "1"
data["two"] = "2"
_, err := k.InitializeConfigMap("loftsman-tests", "default", data)
_, err := k.InitializeShipConfigMap("loftsman-tests", "default", data)
if err != nil {
t.Errorf("Got unexpected error from kubernetes.TestInitializeConfigMapExists(): %s", err)
}
}

func TestInitializeLogConfigMapNew(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", `=~http://loftsman-tests`, httpmock.NewStringResponder(404, `{}`))
httpmock.RegisterResponder("POST", `=~http://loftsman-tests`, httpmock.NewStringResponder(200, `{}`))
k := &Kubernetes{}
_ = k.Initialize("./.test-fixtures/kubeconfig.yaml", "default")
data := make(map[string]string)
data["one"] = "1"
data["two"] = "2"
_, err := k.InitializeLogConfigMap("loftsman-tests-ship-log", "default", data)
if err != nil {
t.Errorf("Got unexpected error from kubernetes.TestInitializeLogConfigMapNew(): %s", err)
}
}

func TestInitializeLogConfigMapExists(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", `=~http://loftsman-tests`, httpmock.NewStringResponder(200, `{"metadata": {"annotations": {}}}`))
httpmock.RegisterResponder("POST", `=~http://loftsman-tests`, httpmock.NewStringResponder(200, `{}`))
httpmock.RegisterResponder("PATCH", `=~http://loftsman-tests`, httpmock.NewStringResponder(200, `{}`))
k := &Kubernetes{}
_ = k.Initialize("./.test-fixtures/kubeconfig.yaml", "default")
data := make(map[string]string)
data["one"] = "1"
data["two"] = "2"
_, err := k.InitializeLogConfigMap("loftsman-tests-ship-log", "default", data)
if err != nil {
t.Errorf("Got unexpected error from kubernetes.TestInitializeLogConfigMapExists(): %s", err)
}
}
func TestPatchConfigMap(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
Expand Down
51 changes: 37 additions & 14 deletions internal/loftsman.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const (
statusCancelled = "cancelled"
statusCrashed = "crashed"
statusAvasted = "avasted"
configMapNameTemplate = "loftsman-%s"
shipConfigMapNameTemplate = "loftsman-%s"
logConfigMapNameTemplate = "loftsman-%s-ship-log"
)

// To reduce the need for always initializing cluster connectivity and internal objects
Expand Down Expand Up @@ -119,14 +120,17 @@ func (loftsman *Loftsman) Ship() error {

loftsman.logger.Header("Shipping your Helm workloads with Loftsman")

configMapName := fmt.Sprintf(configMapNameTemplate, loftsman.Settings.Manifest.Name)
configMapData := make(map[string]string)
shipConfigMapName := fmt.Sprintf(shipConfigMapNameTemplate, loftsman.Settings.Manifest.Name)
shipConfigMapData := make(map[string]string)
logConfigMapName := fmt.Sprintf(logConfigMapNameTemplate, loftsman.Settings.Manifest.Name)
logConfigMapData := make(map[string]string)

loftsman.logger.Info().Msgf("Ensuring that the %s namespace exists", loftsman.Settings.Namespace)
if err = loftsman.kubernetes.EnsureNamespace(loftsman.Settings.Namespace); err != nil {
return loftsman.fail(fmt.Errorf("Error ensuring that the %s namespace exists: %s", loftsman.Settings.Namespace, err))
}
activeConfigMap, err := loftsman.kubernetes.FindConfigMap(configMapName, loftsman.Settings.Namespace, statusKey, statusActive)

activeConfigMap, err := loftsman.kubernetes.FindConfigMap(shipConfigMapName, loftsman.Settings.Namespace, statusKey, statusActive)
if err != nil {
return loftsman.fail(fmt.Errorf("Error determining if another loftsman ship is in progress for manifest %s: %s", loftsman.Settings.Manifest.Name, err))
}
Expand All @@ -147,20 +151,25 @@ func (loftsman *Loftsman) Ship() error {

loftsman.logger.Info().Msgf("Running a release for the provided manifest at %s", loftsman.Settings.Manifest.Path)

configMapData[statusKey] = statusActive
shipConfigMapData[statusKey] = statusActive
sigChannel := make(chan os.Signal)
signal.Notify(sigChannel, os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
go func() {
<-sigChannel
loftsman.recordShipResult(configMapName, configMapData, statusCancelled)
loftsman.recordShipResult(shipConfigMapName, shipConfigMapData, statusCancelled)
loftsman.recordShipLog(logConfigMapName, logConfigMapData)
os.Exit(0)
}()
if _, err := loftsman.kubernetes.InitializeConfigMap(configMapName, loftsman.Settings.Namespace, configMapData); err != nil {
return loftsman.fail(fmt.Errorf("Error creating ship configmap %s in namespace %s: %s", configMapName, loftsman.Settings.Namespace, err))
if _, err := loftsman.kubernetes.InitializeShipConfigMap(shipConfigMapName, loftsman.Settings.Namespace, shipConfigMapData); err != nil {
return loftsman.fail(fmt.Errorf("Error creating ship configmap %s in namespace %s: %s", shipConfigMapName, loftsman.Settings.Namespace, err))
}
if _, err := loftsman.kubernetes.InitializeLogConfigMap(logConfigMapName, loftsman.Settings.Namespace, logConfigMapData); err != nil {
return loftsman.fail(fmt.Errorf("Error creating log configmap %s in namespace %s: %s", logConfigMapName, loftsman.Settings.Namespace, err))
}
crashHandler := func() {
if r := recover(); r != nil {
loftsman.recordShipResult(configMapName, configMapData, statusCrashed)
loftsman.recordShipResult(shipConfigMapName, shipConfigMapData, statusCrashed)
loftsman.recordShipLog(logConfigMapName, logConfigMapData)
loftsman.fail(fmt.Errorf("%v", r))
}
}
Expand All @@ -172,7 +181,8 @@ func (loftsman *Loftsman) Ship() error {
if len(releaseErrors) > 0 {
releaseStatus = statusFailed
}
loftsman.recordShipResult(configMapName, configMapData, releaseStatus)
loftsman.recordShipResult(shipConfigMapName, shipConfigMapData, releaseStatus)
loftsman.recordShipLog(logConfigMapName, logConfigMapData)

if len(releaseErrors) > 0 {
loftsman.logger.ClosingHeader("Encountered errors during the manifest release:")
Expand All @@ -190,10 +200,9 @@ func (loftsman *Loftsman) Ship() error {
}

func (loftsman *Loftsman) recordShipResult(configMapName string, configMapData map[string]string, status string) {
loftsman.logger.Info().Msgf("Ship status: %s. Recording status, manifest, and log data to configmap %s in namespace %s", status,
configMapName, loftsman.Settings.Namespace)
loftsman.logger.Info().Msgf("Ship status: %s. Recording status, manifest to configmap %s in namespace %s", status,
configMapName, loftsman.Settings.Namespace)
configMapData["manifest.yaml"] = string(loftsman.Settings.Manifest.Content)
configMapData["loftsman.log"] = loftsman.logger.GetRecord()
configMapData["status"] = status
if _, err := loftsman.kubernetes.PatchConfigMap(configMapName, loftsman.Settings.Namespace, configMapData); err != nil {
loftsman.logger.Error().Err(fmt.Errorf("Error patching configmap %s with result, manifest, and log data to the %s namespace: %s",
Expand All @@ -202,6 +211,20 @@ func (loftsman *Loftsman) recordShipResult(configMapName string, configMapData m
}
}

func (loftsman *Loftsman) recordShipLog(configMapName string, configMapData map[string]string) {
loftsman.logger.Info().Msgf("Recording log data to configmap %s in namespace %s",
configMapName, loftsman.Settings.Namespace)

logConfigMapData := make(map[string]string)
logConfigMapData["loftsman.log"] = loftsman.logger.GetRecord()

if _, err := loftsman.kubernetes.PatchConfigMap(configMapName, loftsman.Settings.Namespace, logConfigMapData); err != nil {
loftsman.logger.Error().Err(fmt.Errorf("Error patching configmap %s with log data to the %s namespace: %s",
configMapName, loftsman.Settings.Namespace, err)).Msg("")
fmt.Println("")
}
}

// ManifestCreate will create a new manifest and output it to stdout
func (loftsman *Loftsman) ManifestCreate() error {
var err error
Expand Down Expand Up @@ -255,7 +278,7 @@ func (loftsman *Loftsman) Avast() error {
return nil
}

configMapName := fmt.Sprintf(configMapNameTemplate, loftsman.Settings.Manifest.Name)
configMapName := fmt.Sprintf(shipConfigMapNameTemplate, loftsman.Settings.Manifest.Name)

activeConfigMap, err := loftsman.kubernetes.FindConfigMap(configMapName, loftsman.Settings.Namespace, statusKey, statusActive)
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion mocks/custom-mocks/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ func GetKubernetesMock(triggerFoundConfigMap bool) *kubernetesmocks.Kubernetes {
}
return nil
}, nil)
k.On("InitializeConfigMap", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("map[string]string")).Return(&v1.ConfigMap{}, nil)
k.On("InitializeShipConfigMap", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("map[string]string")).Return(&v1.ConfigMap{}, nil)
k.On("InitializeLogConfigMap", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("map[string]string")).Return(&v1.ConfigMap{}, nil)
k.On("PatchConfigMap", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("map[string]string")).Return(&v1.ConfigMap{}, nil)
k.On("GetSecretKeyValue", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(TestSecretKeyValue, nil)
return k
Expand Down
2 changes: 1 addition & 1 deletion mocks/internal/interfaces/Helm.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 26 additions & 3 deletions mocks/internal/interfaces/Kubernetes.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mocks/internal/interfaces/Manifest.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 04db488

Please sign in to comment.