Skip to content

Commit

Permalink
feat: helm recipe
Browse files Browse the repository at this point in the history
  • Loading branch information
huf1 committed Jun 30, 2022
1 parent ee70780 commit f837994
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 0 deletions.
20 changes: 20 additions & 0 deletions pkg/recipes/helm/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package helm

import "github.com/conplementag/cops-hq/pkg/commands"

// New creates a new instance of Helm, which is a wrapper around common Helm functionality.
// Parameters:
// executor (can be provided from hq.GetExecutor() or by instantiating your own),
// namespace (the kubernetes namespace to deploy to),
// chartName (the chart name of the helm deployment),
// helmDirectory (directory where your helm resources are stored. To construct the full path, simply use
// filepath.Join() method and the hq.ProjectBasePath)
func New(executor commands.Executor, namespace string, chartName string, helmDirectory string) Helm {

return &helmWrapper{
executor: executor,
namespace: namespace,
chartName: chartName,
helmDirectory: helmDirectory,
}
}
94 changes: 94 additions & 0 deletions pkg/recipes/helm/helm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package helm

import (
"fmt"
"github.com/conplementag/cops-hq/internal"
"github.com/conplementag/cops-hq/pkg/commands"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"io/ioutil"
"path/filepath"
)

// Helm is a wrapper around common helm functionality used for automated app deployments to kubernetes.
type Helm interface {

// SetVariables set the variables for the helm deployment. Variables set will be applied on any subsequent operation. If you do not provide
// any variables with SetVariables, the default variables in file values.yaml are used.
//
// Parameters support are
// helmVariables (this is a map of helm variables, defined as string keys and interface values. Nested structures are supported)
SetVariables(helmVariables map[string]interface{}) error

// Deploy deploys the helm charts provided in the helmDirectory to the configured kubernetes namespace.
Deploy() error

// GetVariablesOverrideFileName returns the file name in which the helm variables will be stored. This name is convention based on the helm tool chain.
GetVariablesOverrideFileName() string
}

type helmWrapper struct {
executor commands.Executor

namespace string
chartName string
helmDirectory string

variablesSet bool
}

func (h *helmWrapper) SetVariables(helmVariables map[string]interface{}) error {
if helmVariables != nil {
logrus.Info("Setting the helm variables...")

data, err := yaml.Marshal(&helmVariables)

if err != nil {
return internal.ReturnErrorOrPanic(err)
}

// file permission: owner: r,w - group: r - other: r -> 0644
err = ioutil.WriteFile(h.getValuesOverrideFilePath(), data, 0644)

if err != nil {
return internal.ReturnErrorOrPanic(err)
}

h.variablesSet = true
}

return nil
}

func (h *helmWrapper) Deploy() error {
helmCmd := fmt.Sprintf(fmt.Sprintf("helm upgrade --namespace %s --install %s %s -f %s", h.namespace, h.chartName, h.helmDirectory,
h.getValuesFilePath()))

if h.variablesSet {
helmCmd = fmt.Sprintf("%s -f %s", helmCmd, h.getValuesOverrideFilePath())
}

_, err := h.executor.Execute(helmCmd)

if err != nil {
return internal.ReturnErrorOrPanic(err)
}

return nil
}

func (h *helmWrapper) GetVariablesOverrideFileName() string {
return "values.override.yaml"
}

func (h *helmWrapper) getValuesFilePath() string {
return filepath.Join(h.helmDirectory, getVariablesFileName())
}

func (h *helmWrapper) getValuesOverrideFilePath() string {
return filepath.Join(h.helmDirectory, h.GetVariablesOverrideFileName())
}

func getVariablesFileName() string {
return "values.yaml"
}
140 changes: 140 additions & 0 deletions pkg/recipes/helm/helm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package helm

import (
"github.com/conplementag/cops-hq/pkg/commands"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
)

type executorMock struct {
mock.Mock
commands.Executor
}

func Test_SetVariablesCanCreateVariablesFile(t *testing.T) {
// Arrange
h, _ := createSimpleHelmWithDefaultSettings("test")

var variables = make(map[string]interface{})

variables["simple"] = "value"
variables["simpleNumber"] = 3
variables["truth"] = false
variables["nested"] = map[string]interface{}{
"key1": "value1_nested",
"key2": 3}
variables["list_of_strings"] = []string{"10.0.0.1", "20.20.20.4"}
variables["empty_array"] = []string{}

// Act
err := h.SetVariables(variables)

// Assert
assert.NoError(t, err)

filePath := filepath.Join(".", h.GetVariablesOverrideFileName())
fileBytes, err := ioutil.ReadFile(filePath)

if err != nil {
assert.NoError(t, err)
}

// the strings below are the exact terraform format
assert.Contains(t, string(fileBytes), "simple: value")
assert.Contains(t, string(fileBytes), "simpleNumber: 3")
assert.Contains(t, string(fileBytes), "truth: false")
assert.Contains(t, string(fileBytes), "nested:")
assert.Contains(t, string(fileBytes), "key1: value1_nested")
assert.Contains(t, string(fileBytes), "key2: 3")
assert.Contains(t, string(fileBytes), "list_of_strings:")
assert.Contains(t, string(fileBytes), "- 10.0.0.1")
assert.Contains(t, string(fileBytes), "empty_array")
assert.Contains(t, string(fileBytes), "[]")

// Cleanup
if existsFile(filePath) {
os.Remove(filePath)
}
}

func Test_DeployExecutesExpectedCommandWithOverrideValues(t *testing.T) {
h, executorMock := createSimpleHelmWithDefaultSettings("project")
// helm upgrade with one value file is expected
executorMock.On("Execute", mock.MatchedBy(func(command string) bool {
return strings.Contains(command, "helm upgrade") && strings.Contains(command, "values.yaml") && strings.Contains(command, "values.override.yaml")
})).Once()

helmVariables := make(map[string]interface{})
helmVariables["test_key"] = "test_value"

h.SetVariables(helmVariables)

// Act
err := h.Deploy()

// Assert
assert.NoError(t, err)

executorMock.AssertExpectations(t)

// Cleanup
filePath := filepath.Join(".", h.GetVariablesOverrideFileName())
if existsFile(filePath) {
os.Remove(filePath)
}
}

func Test_DeployExecutesExpectedCommandWithoutOverrideValues(t *testing.T) {
h, executorMock := createSimpleHelmWithDefaultSettings("project")
// helm upgrade with one value file is expected
executorMock.On("Execute", mock.MatchedBy(func(command string) bool {
return strings.Contains(command, "helm upgrade") && strings.Contains(command, "values.yaml") && !strings.Contains(command, "values.override.yaml")
})).Once()

// Act
err := h.Deploy()

// Assert
assert.NoError(t, err)

executorMock.AssertExpectations(t)
}

func Test_GetVariablesOverrideFileNameReturnsSomething(t *testing.T) {
h, executorMock := createSimpleHelmWithDefaultSettings("project")
// helm upgrade with one value file is expected
executorMock.On("Execute", mock.MatchedBy(func(command string) bool {
return strings.Contains(command, "helm upgrade") && strings.Contains(command, "values.yaml") && !strings.Contains(command, "values.override.yaml")
})).Once()

// Act
actual := h.GetVariablesOverrideFileName()

// Assert
assert.NotEmpty(t, actual)
}

func (e *executorMock) Execute(command string) (string, error) {
e.Called(command)
return "success", nil
}

func createSimpleHelmWithDefaultSettings(projectName string) (Helm, *executorMock) {
executor := &executorMock{}

return New(executor, projectName+"test", projectName+"test", filepath.Join(".")), executor
}

func existsFile(fileName string) bool {
_, error := os.Stat(fileName)
if os.IsNotExist(error) {
return false
} else {
return true
}
}

0 comments on commit f837994

Please sign in to comment.