-
Notifications
You must be signed in to change notification settings - Fork 169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: provide std.native('helmTemplate') #336
Merged
Merged
Changes from 10 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
5934a80
feat: provide helmTemplate native function
Duologic 7560dc8
cut main func into testable units
Duologic 3f466b7
test the units
Duologic a3e00eb
move helmraiser into separate package
Duologic 06d5f17
remove omitempty
Duologic d742657
make confToArgs self contained
Duologic 11db758
normalizeName on a single line
Duologic aab1161
add HelmConf struct
Duologic 1b6cac6
added multiple files test case
Duologic 25ff9cb
get helm binary through TANKA_HELM_BIN
Duologic bf3d96f
_BIN -> _PATH
Duologic af43930
Restructure error message
Duologic 563101f
document TANKA_HELM_PATH
Duologic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
dist | ||
tk | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
package helmraiser | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"os" | ||
"os/exec" | ||
"strings" | ||
|
||
jsonnet "github.com/google/go-jsonnet" | ||
"github.com/google/go-jsonnet/ast" | ||
"github.com/pkg/errors" | ||
yaml "gopkg.in/yaml.v3" | ||
) | ||
|
||
type HelmConf struct { | ||
Values map[string]interface{} | ||
Flags []string | ||
} | ||
|
||
func confToArgs(conf HelmConf) ([]string, []string, error) { | ||
var args []string | ||
var tempFiles []string | ||
|
||
// create file and append to args | ||
if len(conf.Values) != 0 { | ||
valuesYaml, err := yaml.Marshal(conf.Values) | ||
if err != nil { | ||
return nil, nil, err | ||
} | ||
tmpFile, err := ioutil.TempFile(os.TempDir(), "tanka-") | ||
if err != nil { | ||
return nil, nil, errors.Wrap(err, "cannot create temporary values.yaml") | ||
} | ||
tempFiles = append(tempFiles, tmpFile.Name()) | ||
if _, err = tmpFile.Write(valuesYaml); err != nil { | ||
return nil, tempFiles, errors.Wrap(err, "failed to write to temporary values.yaml") | ||
} | ||
if err := tmpFile.Close(); err != nil { | ||
return nil, tempFiles, err | ||
} | ||
args = append(args, fmt.Sprintf("--values=%s", tmpFile.Name())) | ||
} | ||
|
||
// append custom flags to args | ||
args = append(args, conf.Flags...) | ||
|
||
if len(args) == 0 { | ||
args = nil | ||
} | ||
|
||
return args, tempFiles, nil | ||
} | ||
|
||
func parseYamlToMap(yamlFile []byte) (map[string]interface{}, error) { | ||
files := make(map[string]interface{}) | ||
d := yaml.NewDecoder(bytes.NewReader(yamlFile)) | ||
for { | ||
var doc, jsonDoc interface{} | ||
if err := d.Decode(&doc); err != nil { | ||
if err == io.EOF { | ||
break | ||
} | ||
return nil, errors.Wrap(err, "parsing manifests") | ||
} | ||
|
||
jsonRaw, err := json.Marshal(doc) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "marshaling mainfests") | ||
} | ||
|
||
if err := json.Unmarshal(jsonRaw, &jsonDoc); err != nil { | ||
return nil, errors.Wrap(err, "unmarshaling manifests") | ||
} | ||
sh0rez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Unmarshal name and kind | ||
kindName := struct { | ||
Kind string `json:"kind"` | ||
Metadata struct { | ||
Name string `json:"name"` | ||
} `json:"metadata"` | ||
}{} | ||
if err := json.Unmarshal(jsonRaw, &kindName); err != nil { | ||
return nil, errors.Wrap(err, "subtracting kind/name through unmarshaling") | ||
} | ||
|
||
// snake_case string | ||
normalizeName := func(s string) string { | ||
s = strings.ReplaceAll(s, "-", "_") | ||
s = strings.ReplaceAll(s, ":", "_") | ||
s = strings.ToLower(s) | ||
return s | ||
} | ||
|
||
// create a map of resources for ease of use in jsonnet | ||
name := normalizeName(fmt.Sprintf("%s_%s", kindName.Metadata.Name, kindName.Kind)) | ||
if jsonDoc != nil { | ||
files[name] = jsonDoc | ||
} | ||
} | ||
return files, nil | ||
} | ||
|
||
// helmTemplate wraps and runs `helm template` | ||
// returns the generated manifests in a map | ||
func HelmTemplate() *jsonnet.NativeFunction { | ||
return &jsonnet.NativeFunction{ | ||
Name: "helmTemplate", | ||
// Lines up with `helm template [NAME] [CHART] [flags]` except 'conf' is a bit more elaborate | ||
Params: ast.Identifiers{"name", "chart", "conf"}, | ||
Func: func(data []interface{}) (interface{}, error) { | ||
name, chart := data[0].(string), data[1].(string) | ||
|
||
c, err := json.Marshal(data[2]) | ||
if err != nil { | ||
return "", err | ||
} | ||
var conf HelmConf | ||
if err := json.Unmarshal(c, &conf); err != nil { | ||
return "", err | ||
} | ||
|
||
// the basic arguments to make this work | ||
args := []string{ | ||
"template", | ||
name, | ||
chart, | ||
} | ||
|
||
confArgs, tempFiles, err := confToArgs(conf) | ||
if err != nil { | ||
return "", nil | ||
} | ||
for _, file := range tempFiles { | ||
defer os.Remove(file) | ||
} | ||
if confArgs != nil { | ||
args = append(args, confArgs...) | ||
} | ||
|
||
helmBinary := "helm" | ||
if hc := os.Getenv("TANKA_HELM_BIN"); hc != "" { | ||
Duologic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
helmBinary = hc | ||
} | ||
|
||
// convert the values map into a yaml file | ||
cmd := exec.Command(helmBinary, args...) | ||
buf := bytes.Buffer{} | ||
cmd.Stdout = &buf | ||
cmd.Stderr = os.Stderr | ||
if err := cmd.Run(); err != nil { | ||
return nil, errors.Wrap(err, fmt.Sprintf("while running helm %s", strings.Join(args, " "))) | ||
Duologic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
return parseYamlToMap(buf.Bytes()) | ||
}, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
package helmraiser | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestConfToArgs_noconf(t *testing.T) { | ||
conf := HelmConf{} | ||
args, tempFiles, err := confToArgs(conf) | ||
for _, file := range tempFiles { | ||
defer os.Remove(file) | ||
} | ||
|
||
assert.Equal(t, []string(nil), args) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestConfToArgs_emptyconf(t *testing.T) { | ||
conf := HelmConf{ | ||
Values: map[string]interface{}{}, | ||
Flags: []string{}, | ||
} | ||
|
||
args, tempFiles, err := confToArgs(conf) | ||
for _, file := range tempFiles { | ||
defer os.Remove(file) | ||
} | ||
|
||
assert.Equal(t, []string(nil), args) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestConfToArgs_flags(t *testing.T) { | ||
conf := HelmConf{ | ||
Flags: []string{ | ||
"--version=v0.1", | ||
"--random=arg", | ||
}, | ||
} | ||
|
||
args, tempFiles, err := confToArgs(conf) | ||
for _, file := range tempFiles { | ||
defer os.Remove(file) | ||
} | ||
|
||
assert.Equal(t, []string{ | ||
"--version=v0.1", | ||
"--random=arg", | ||
}, args) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestConfToArgs_values(t *testing.T) { | ||
conf := HelmConf{ | ||
Values: map[string]interface{}{ | ||
"hasValues": "yes", | ||
}, | ||
} | ||
|
||
args, tempFiles, err := confToArgs(conf) | ||
for _, file := range tempFiles { | ||
defer os.Remove(file) | ||
} | ||
|
||
assert.FileExists(t, tempFiles[0]) | ||
assert.Equal(t, []string{fmt.Sprintf("--values=%s", tempFiles[0])}, args) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestConfToArgs_flagsvalues(t *testing.T) { | ||
conf := HelmConf{ | ||
Values: map[string]interface{}{ | ||
"hasValues": "yes", | ||
}, | ||
Flags: []string{ | ||
"--version=v0.1", | ||
"--random=arg", | ||
}, | ||
} | ||
|
||
args, tempFiles, err := confToArgs(conf) | ||
for _, file := range tempFiles { | ||
defer os.Remove(file) | ||
} | ||
|
||
assert.Equal(t, []string{ | ||
fmt.Sprintf("--values=%s", tempFiles[0]), | ||
"--version=v0.1", | ||
"--random=arg", | ||
}, args) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestParseYamlToMap_basic(t *testing.T) { | ||
yamlFile := []byte(`--- | ||
kind: testKind | ||
metadata: | ||
name: testName`) | ||
actual, err := parseYamlToMap(yamlFile) | ||
|
||
expected := map[string]interface{}{ | ||
"testname_testkind": map[string]interface{}{ | ||
"kind": "testKind", | ||
"metadata": map[string]interface{}{ | ||
"name": "testName", | ||
}, | ||
}, | ||
} | ||
assert.Equal(t, expected, actual) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestParseYamlToMap_dash(t *testing.T) { | ||
yamlFile := []byte(`--- | ||
kind: testKind | ||
metadata: | ||
name: test-Name`) | ||
actual, err := parseYamlToMap(yamlFile) | ||
|
||
expected := map[string]interface{}{ | ||
"test_name_testkind": map[string]interface{}{ | ||
"kind": "testKind", | ||
"metadata": map[string]interface{}{ | ||
"name": "test-Name", | ||
}, | ||
}, | ||
} | ||
assert.Equal(t, expected, actual) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestParseYamlToMap_colon(t *testing.T) { | ||
yamlFile := []byte(`--- | ||
kind: testKind | ||
metadata: | ||
name: test:Name`) | ||
actual, err := parseYamlToMap(yamlFile) | ||
|
||
expected := map[string]interface{}{ | ||
"test_name_testkind": map[string]interface{}{ | ||
"kind": "testKind", | ||
"metadata": map[string]interface{}{ | ||
"name": "test:Name", | ||
}, | ||
}, | ||
} | ||
assert.Equal(t, expected, actual) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestParseYamlToMap_empty(t *testing.T) { | ||
yamlFile := []byte(`---`) | ||
actual, err := parseYamlToMap(yamlFile) | ||
|
||
expected := map[string]interface{}{} | ||
assert.Equal(t, expected, actual) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestParseYamlToMap_multiple_files(t *testing.T) { | ||
yamlFile := []byte(`--- | ||
kind: testKind | ||
metadata: | ||
name: testName | ||
--- | ||
kind: testKind | ||
metadata: | ||
name: testName2`) | ||
actual, err := parseYamlToMap(yamlFile) | ||
|
||
expected := map[string]interface{}{ | ||
"testname_testkind": map[string]interface{}{ | ||
"kind": "testKind", | ||
"metadata": map[string]interface{}{ | ||
"name": "testName", | ||
}, | ||
}, | ||
"testname2_testkind": map[string]interface{}{ | ||
"kind": "testKind", | ||
"metadata": map[string]interface{}{ | ||
"name": "testName2", | ||
}, | ||
}, | ||
} | ||
assert.Equal(t, expected, actual) | ||
assert.Nil(t, err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You only ever add one file this this, right? No need for a slice then
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm optimizing for future cases where we might want to pass multiple files (it is possible to have multiple --values with
helm template
) and can easily extend this without having to change this elsewhere. If you don't see value in this, I'll remove it.