Skip to content

feat: safer context parsing, general refactoring #228

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

Merged
merged 7 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cmd/tk/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import (

"text/template"

"github.com/grafana/tanka/pkg/tanka"
"github.com/spf13/cobra"

"github.com/grafana/tanka/pkg/tanka"
)

func exportCmd() *cobra.Command {
Expand Down Expand Up @@ -57,7 +58,9 @@ func exportCmd() *cobra.Command {
// write each to a file
for _, m := range res {
buf := bytes.Buffer{}
tmpl.Execute(&buf, m)
if err := tmpl.Execute(&buf, m); err != nil {
log.Fatalln("executing name template:", err)
}
name := strings.Replace(buf.String(), "/", "-", -1)

data := m.String()
Expand Down
2 changes: 1 addition & 1 deletion cmd/tk/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func initCmd() *cobra.Command {

func installK8sLib() error {
if _, err := exec.LookPath("jb"); err != nil {
return errors.New("jsonnet-bundler not found in $PATH. Follow https://tanka.dev/install#jsonnet-bundler for installation instructions.")
return errors.New("jsonnet-bundler not found in $PATH. Follow https://tanka.dev/install#jsonnet-bundler for installation instructions")
}

var initialPackages = []string{
Expand Down
5 changes: 3 additions & 2 deletions cmd/tk/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ func statusCmd() *cobra.Command {
log.Fatalln(err)
}

fmt.Println("Context:", status.Client.Context.Get("name"))
fmt.Println("Cluster:", status.Client.Context.Get("context").MustObjxMap().Get("cluster"))
context := status.Client.Kubeconfig.Context
fmt.Println("Context:", context.Name)
fmt.Println("Cluster:", context.Context.Cluster)
fmt.Println("Environment:")
for k, v := range structs.Map(status.Env.Spec) {
fmt.Printf(" %s: %s\n", k, v)
Expand Down
5 changes: 4 additions & 1 deletion cmd/tk/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
Expand Down Expand Up @@ -36,7 +37,9 @@ func fPageln(r io.Reader) {

// if this fails, just print it
if err := cmd.Run(); err != nil {
io.Copy(os.Stdout, r)
if _, err = io.Copy(os.Stdout, r); err != nil {
log.Fatalln("Writing to Stdout:", err)
}
}
}

Expand Down
6 changes: 2 additions & 4 deletions pkg/cli/cmp/complete.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ func (h CompletionHandlers) Has(k string) bool {
}

// Handlers are global Handlers to be used in annotations
var Handlers = CompletionHandlers{}

func init() {
Handlers["dirs"] = complete.PredictDirs("*")
var Handlers = CompletionHandlers{
"dirs": complete.PredictDirs("*"),
}

// Create parses a *cobra.Command into a complete.Command
Expand Down
2 changes: 1 addition & 1 deletion pkg/jsonnet/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/grafana/tanka/pkg/jsonnet/native"
)

// Modifiers allow to set optional paramters on the Jsonnet VM.
// Modifier allows to set optional parameters on the Jsonnet VM.
// See jsonnet.With* for this.
type Modifier func(vm *jsonnet.VM) error

Expand Down
196 changes: 105 additions & 91 deletions pkg/jsonnet/native/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,127 +18,141 @@ import (
func Funcs() []*jsonnet.NativeFunction {
return []*jsonnet.NativeFunction{
// Parse serialized data into dicts
parseJSON,
parseYAML,
parseJSON(),
parseYAML(),

// Convert serializations
manifestJSONFromJSON,
manifestYAMLFromJSON,
manifestJSONFromJSON(),
manifestYAMLFromJSON(),

// Regular expressions
escapeStringRegex,
regexMatch,
regexSubst,
escapeStringRegex(),
regexMatch(),
regexSubst(),
}
}

// parseJSON wraps `json.Unmarshal` to convert a json string into a dict
var parseJSON = &jsonnet.NativeFunction{
Name: "parseJson",
Params: ast.Identifiers{"json"},
Func: func(dataString []interface{}) (res interface{}, err error) {
data := []byte(dataString[0].(string))
err = json.Unmarshal(data, &res)
return
},
func parseJSON() *jsonnet.NativeFunction {
return &jsonnet.NativeFunction{
Name: "parseJson",
Params: ast.Identifiers{"json"},
Func: func(dataString []interface{}) (res interface{}, err error) {
data := []byte(dataString[0].(string))
err = json.Unmarshal(data, &res)
return
},
}
}

// parseYAML wraps `yaml.Unmarshal` to convert a string of yaml document(s) into a (set of) dicts
var parseYAML = &jsonnet.NativeFunction{
Name: "parseYaml",
Params: ast.Identifiers{"yaml"},
Func: func(dataString []interface{}) (interface{}, error) {
data := []byte(dataString[0].(string))
ret := []interface{}{}

d := yaml.NewDecoder(bytes.NewReader(data))
for {
var doc, jsonDoc interface{}
if err := d.Decode(&doc); err != nil {
if err == io.EOF {
break
func parseYAML() *jsonnet.NativeFunction {
return &jsonnet.NativeFunction{
Name: "parseYaml",
Params: ast.Identifiers{"yaml"},
Func: func(dataString []interface{}) (interface{}, error) {
data := []byte(dataString[0].(string))
ret := []interface{}{}

d := yaml.NewDecoder(bytes.NewReader(data))
for {
var doc, jsonDoc interface{}
if err := d.Decode(&doc); err != nil {
if err == io.EOF {
break
}
return nil, errors.Wrap(err, "parsing yaml")
}
return nil, errors.Wrap(err, "parsing yaml")
}

jsonRaw, err := json.Marshal(doc)
if err != nil {
return nil, errors.Wrap(err, "converting yaml to json")
}
jsonRaw, err := json.Marshal(doc)
if err != nil {
return nil, errors.Wrap(err, "converting yaml to json")
}

if err := json.Unmarshal(jsonRaw, &jsonDoc); err != nil {
return nil, errors.Wrap(err, "converting yaml to json")
}
if err := json.Unmarshal(jsonRaw, &jsonDoc); err != nil {
return nil, errors.Wrap(err, "converting yaml to json")
}

ret = append(ret, jsonDoc)
}
ret = append(ret, jsonDoc)
}

return ret, nil
},
return ret, nil
},
}
}

// manifestJSONFromJSON reserializes JSON which allows to change the indentation.
var manifestJSONFromJSON = &jsonnet.NativeFunction{
Name: "manifestJsonFromJson",
Params: ast.Identifiers{"json", "indent"},
Func: func(data []interface{}) (interface{}, error) {
indent := int(data[1].(float64))
dataBytes := []byte(data[0].(string))
dataBytes = bytes.TrimSpace(dataBytes)
buf := bytes.Buffer{}
if err := json.Indent(&buf, dataBytes, "", strings.Repeat(" ", indent)); err != nil {
return "", err
}
buf.WriteString("\n")
return buf.String(), nil
},
func manifestJSONFromJSON() *jsonnet.NativeFunction {
return &jsonnet.NativeFunction{
Name: "manifestJsonFromJson",
Params: ast.Identifiers{"json", "indent"},
Func: func(data []interface{}) (interface{}, error) {
indent := int(data[1].(float64))
dataBytes := []byte(data[0].(string))
dataBytes = bytes.TrimSpace(dataBytes)
buf := bytes.Buffer{}
if err := json.Indent(&buf, dataBytes, "", strings.Repeat(" ", indent)); err != nil {
return "", err
}
buf.WriteString("\n")
return buf.String(), nil
},
}
}

// manifestYamlFromJSON serializes a JSON string as a YAML document
var manifestYAMLFromJSON = &jsonnet.NativeFunction{
Name: "manifestYamlFromJson",
Params: ast.Identifiers{"json"},
Func: func(data []interface{}) (interface{}, error) {
var input interface{}
dataBytes := []byte(data[0].(string))
if err := json.Unmarshal(dataBytes, &input); err != nil {
return "", err
}
output, err := yaml.Marshal(input)
return string(output), err
},
func manifestYAMLFromJSON() *jsonnet.NativeFunction {
return &jsonnet.NativeFunction{
Name: "manifestYamlFromJson",
Params: ast.Identifiers{"json"},
Func: func(data []interface{}) (interface{}, error) {
var input interface{}
dataBytes := []byte(data[0].(string))
if err := json.Unmarshal(dataBytes, &input); err != nil {
return "", err
}
output, err := yaml.Marshal(input)
return string(output), err
},
}
}

// escapeStringRegex escapes all regular expression metacharacters
// and returns a regular expression that matches the literal text.
var escapeStringRegex = &jsonnet.NativeFunction{
Name: "escapeStringRegex",
Params: ast.Identifiers{"str"},
Func: func(s []interface{}) (interface{}, error) {
return regexp.QuoteMeta(s[0].(string)), nil
},
func escapeStringRegex() *jsonnet.NativeFunction {
return &jsonnet.NativeFunction{
Name: "escapeStringRegex",
Params: ast.Identifiers{"str"},
Func: func(s []interface{}) (interface{}, error) {
return regexp.QuoteMeta(s[0].(string)), nil
},
}
}

// regexMatch returns whether the given string is matched by the given re2 regular expression.
var regexMatch = &jsonnet.NativeFunction{
Name: "regexMatch",
Params: ast.Identifiers{"regex", "string"},
Func: func(s []interface{}) (interface{}, error) {
return regexp.MatchString(s[0].(string), s[1].(string))
},
func regexMatch() *jsonnet.NativeFunction {
return &jsonnet.NativeFunction{
Name: "regexMatch",
Params: ast.Identifiers{"regex", "string"},
Func: func(s []interface{}) (interface{}, error) {
return regexp.MatchString(s[0].(string), s[1].(string))
},
}
}

// regexSubst replaces all matches of the re2 regular expression with another string.
var regexSubst = &jsonnet.NativeFunction{
Name: "regexSubst",
Params: ast.Identifiers{"regex", "src", "repl"},
Func: func(data []interface{}) (interface{}, error) {
regex, src, repl := data[0].(string), data[1].(string), data[2].(string)

r, err := regexp.Compile(regex)
if err != nil {
return "", err
}
return r.ReplaceAllString(src, repl), nil
},
func regexSubst() *jsonnet.NativeFunction {
return &jsonnet.NativeFunction{
Name: "regexSubst",
Params: ast.Identifiers{"regex", "src", "repl"},
Func: func(data []interface{}) (interface{}, error) {
regex, src, repl := data[0].(string), data[1].(string), data[2].(string)

r, err := regexp.Compile(regex)
if err != nil {
return "", err
}
return r.ReplaceAllString(src, repl), nil
},
}
}
36 changes: 36 additions & 0 deletions pkg/kubernetes/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package kubernetes

import (
"fmt"

"github.com/fatih/color"

"github.com/grafana/tanka/pkg/cli"
"github.com/grafana/tanka/pkg/kubernetes/client"
"github.com/grafana/tanka/pkg/kubernetes/manifest"
)

// ApplyOpts allow set additional parameters for the apply operation
type ApplyOpts client.ApplyOpts

// Apply receives a state object generated using `Reconcile()` and may apply it to the target system
func (k *Kubernetes) Apply(state manifest.List, opts ApplyOpts) error {
alert := color.New(color.FgRed, color.Bold).SprintFunc()

cluster := k.ctl.Info().Kubeconfig.Cluster
context := k.ctl.Info().Kubeconfig.Context
if !opts.AutoApprove {
if err := cli.Confirm(
fmt.Sprintf(`Applying to namespace '%s' of cluster '%s' at '%s' using context '%s'.`,
alert(k.Spec.Namespace),
alert(cluster.Name),
alert(cluster.Cluster.Server),
alert(context.Name),
),
"yes",
); err != nil {
return err
}
}
return k.ctl.Apply(state, client.ApplyOpts(opts))
}
3 changes: 2 additions & 1 deletion pkg/kubernetes/client/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"os"
"strings"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
funk "github.com/thoas/go-funk"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
)

// Apply applies the given yaml to the cluster
Expand Down
16 changes: 2 additions & 14 deletions pkg/kubernetes/client/client.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package client

import (
"github.com/Masterminds/semver"
"github.com/stretchr/objx"

"github.com/grafana/tanka/pkg/kubernetes/manifest"
)

Expand Down Expand Up @@ -32,18 +29,9 @@ type Client interface {
// fields of `Info` that cannot be stocked with valuable data, e.g.
// due to an error, shall be left nil.
Info() Info
}

// Info contains metadata about the client and its environment
type Info struct {
// version of `kubectl`
ClientVersion *semver.Version

// version of the API server
ServerVersion *semver.Version

// used context and cluster from KUBECONFIG
Context, Cluster objx.Map
// Close may run tasks once the client is no longer needed.
Close() error
}

// ApplyOpts allow to specify additional parameter for apply operations
Expand Down
Loading