Skip to content

Commit

Permalink
refactor: api (#97)
Browse files Browse the repository at this point in the history
Refactors all of the actual application logic out of the main package, into the `github.com/grafana/tanka/pkg/tanka` package, which effectively forms an API for Tanka, allowing it to be imported into other projects and used from there.

The API is loosely based on the CLI, ie it follows the same scheme:

    tanka.Apply("environments/default")

To change the behaviour of these calls, for example the diff strategy, modifiers can be used (https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis):

    tanka.Apply("environments/default", tanka.WithApplyForce(true))

Does some further cleanup on the whole repository, ie implementing linter suggestions and moving the `main` package from the former global based approach to the new package, which should resolve `nil` receiver panics for workflow operations forever.
  • Loading branch information
sh0rez authored Oct 29, 2019
1 parent 13238e5 commit c5edb8b
Show file tree
Hide file tree
Showing 20 changed files with 333 additions and 198 deletions.
19 changes: 11 additions & 8 deletions cmd/tk/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import (
"path/filepath"
"text/tabwriter"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"

"github.com/grafana/tanka/pkg/cli"
"github.com/grafana/tanka/pkg/spec/v1alpha1"
"github.com/grafana/tanka/pkg/util"
)

func envCmd() *cobra.Command {
Expand Down Expand Up @@ -50,7 +51,7 @@ func envSetCmd() *cobra.Command {
envSettingsFlags(&tmp, cmd.Flags())

name := cmd.Flags().String("name", "", "")
cmd.Flags().MarkHidden("name")
_ = cmd.Flags().MarkHidden("name")

cmd.Run = func(cmd *cobra.Command, args []string) {
if *name != "" {
Expand Down Expand Up @@ -115,14 +116,16 @@ func addEnv(dir string, cfg *v1alpha1.Config) error {
if _, err := os.Stat(path); err != nil {
// folder does not exist
if os.IsNotExist(err) {
os.MkdirAll(path, os.ModePerm)
if err := os.MkdirAll(path, os.ModePerm); err != nil {
return errors.Wrap(err, "creating directory")
}
} else {
// it exists
if os.IsExist(err) {
return fmt.Errorf("Directory %s already exists.", path)
return fmt.Errorf("directory %s already exists", path)
}
// we have another error
return fmt.Errorf("Creating directory: %s", err)
return errors.Wrap(err, "creating directory")
}
}

Expand Down Expand Up @@ -156,7 +159,7 @@ func envRemoveCmd() *cobra.Command {
if err != nil {
log.Fatalln("parsing environments name:", err)
}
if err := util.Confirm(fmt.Sprintf("Permanently removing the environment located at '%s'.", path), "yes"); err != nil {
if err := cli.Confirm(fmt.Sprintf("Permanently removing the environment located at '%s'.", path), "yes"); err != nil {
log.Fatalln(err)
}
if err := os.RemoveAll(path); err != nil {
Expand All @@ -179,7 +182,7 @@ func envListCmd() *cobra.Command {
cmd.Run = func(cmd *cobra.Command, args []string) {
envs := []v1alpha1.Config{}
dirs := findBaseDirs()
useJson, err := cmd.Flags().GetBool("json")
useJSON, err := cmd.Flags().GetBool("json")
if err != nil {
// this err should never occur. Panic in case
panic(err)
Expand All @@ -189,7 +192,7 @@ func envListCmd() *cobra.Command {
envs = append(envs, *setupConfiguration(dir))
}

if useJson {
if useJSON {
j, err := json.Marshal(envs)
if err != nil {
log.Fatalln("Formatting as json:", j)
Expand Down
1 change: 0 additions & 1 deletion cmd/tk/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ func initCmd() *cobra.Command {
}

fmt.Println("Directory structure set up! Remember to configure the API endpoint:\n`tk env set environments/default --server=127.0.0.1:6443`")

}
return cmd
}
Expand Down
14 changes: 0 additions & 14 deletions cmd/tk/jsonnet.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"encoding/json"
"log"
"path/filepath"

Expand Down Expand Up @@ -47,16 +46,3 @@ func eval(workdir string) (string, error) {
}
return json, nil
}

func evalDict(workdir string) (map[string]interface{}, error) {
var rawDict map[string]interface{}

raw, err := eval(workdir)
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(raw), &rawDict); err != nil {
return nil, err
}
return rawDict, nil
}
31 changes: 1 addition & 30 deletions cmd/tk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import (
"github.com/spf13/viper"
"golang.org/x/crypto/ssh/terminal"

"github.com/grafana/tanka/pkg/cmp"
"github.com/grafana/tanka/pkg/kubernetes"
"github.com/grafana/tanka/pkg/cli/cmp"
"github.com/grafana/tanka/pkg/spec"
"github.com/grafana/tanka/pkg/spec/v1alpha1"
)
Expand All @@ -21,47 +20,19 @@ import (
// To be overwritten at build time
var Version = "dev"

// primary handlers
var (
config = &v1alpha1.Config{}
kube *kubernetes.Kubernetes
)

// describing variables
var (
verbose = false
interactive = terminal.IsTerminal(int(os.Stdout.Fd()))
)

// list of deprecated config keys and their alternatives
// however, they still work and are aliased internally
var deprecated = map[string]string{
"namespace": "spec.namespace",
"server": "spec.apiServer",
"team": "metadata.labels.team",
}

func main() {
log.SetFlags(0)
rootCmd := &cobra.Command{
Use: "tk",
Short: "tanka <3 jsonnet",
Version: Version,
TraverseChildren: true,
// Configuration
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
return
}
config = setupConfiguration(args[0])
if config == nil {
return
}

// Kubernetes
kube = kubernetes.New(config.Spec)

},
}
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "")

Expand Down
4 changes: 2 additions & 2 deletions cmd/tk/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ func highlight(lang, s string) string {
func writeJSON(i interface{}, path string) error {
out, err := json.MarshalIndent(i, "", " ")
if err != nil {
return fmt.Errorf("Marshalling: %s", err)
return fmt.Errorf("marshalling: %s", err)
}

if err := ioutil.WriteFile(path, append(out, '\n'), 0644); err != nil {
return fmt.Errorf("Writing %s: %s", path, err)
return fmt.Errorf("writing %s: %s", path, err)
}

return nil
Expand Down
117 changes: 37 additions & 80 deletions cmd/tk/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/grafana/tanka/pkg/cmp"
"github.com/grafana/tanka/pkg/kubernetes"
"github.com/grafana/tanka/pkg/cli/cmp"
"github.com/grafana/tanka/pkg/tanka"
)

// special exit codes for tk diff
const (
// no changes
ExitStatusClean = 0
// differences between the local config and the cluster
ExitStatusDiff = 16
)

type workflowFlagVars struct {
Expand All @@ -38,29 +46,13 @@ func applyCmd() *cobra.Command {
force := cmd.Flags().Bool("force", false, "force applying (kubectl apply --force)")
autoApprove := cmd.Flags().Bool("dangerous-auto-approve", false, "skip interactive approval. Only for automation!")
cmd.Run = func(cmd *cobra.Command, args []string) {
if kube == nil {
log.Fatalln(kubernetes.ErrorMissingConfig{Verb: "apply"})
}

raw, err := evalDict(args[0])
err := tanka.Apply(args[0],
tanka.WithTargets(stringsToRegexps(vars.targets)...),
tanka.WithApplyForce(*force),
tanka.WithApplyAutoApprove(*autoApprove),
)
if err != nil {
log.Fatalln("Evaluating jsonnet:", err)
}

desired, err := kube.Reconcile(raw, stringsToRegexps(vars.targets)...)
if err != nil {
log.Fatalln("Reconciling:", err)
}

if !diff(desired, false, kubernetes.DiffOpts{}) {
log.Println("Warning: There are no differences. Your apply may not do anything at all.")
}

if err := kube.Apply(desired, kubernetes.ApplyOpts{
Force: *force,
AutoApprove: *autoApprove,
}); err != nil {
log.Fatalln("Applying:", err)
log.Fatalln(err)
}
}
return cmd
Expand Down Expand Up @@ -88,58 +80,31 @@ func diffCmd() *cobra.Command {
)

cmd.Run = func(cmd *cobra.Command, args []string) {
if kube == nil {
log.Fatalln(kubernetes.ErrorMissingConfig{Verb: "diff"})
}

raw, err := evalDict(args[0])
if err != nil {
log.Fatalln("Evaluating jsonnet:", err)
}

if kube.Spec.DiffStrategy == "" {
kube.Spec.DiffStrategy = *diffStrategy
}

desired, err := kube.Reconcile(raw, stringsToRegexps(vars.targets)...)
changes, err := tanka.Diff(args[0],
tanka.WithTargets(stringsToRegexps(vars.targets)...),
tanka.WithDiffStrategy(*diffStrategy),
tanka.WithDiffSummarize(*summarize),
)
if err != nil {
log.Fatalln("Reconciling:", err)
log.Fatalln(err)
}

if diff(desired, interactive, kubernetes.DiffOpts{Summarize: *summarize}) {
os.Exit(16)
if changes == nil {
log.Println("No differences.")
os.Exit(ExitStatusClean)
}
log.Println("No differences.")
}

return cmd
}

// computes the diff, prints to screen.
// set `pager` to false to disable the pager.
// When non-interactive, no paging happens anyways.
func diff(state []kubernetes.Manifest, pager bool, opts kubernetes.DiffOpts) (changed bool) {
changes, err := kube.Diff(state, opts)
if err != nil {
log.Fatalln("Diffing:", err)
}

if changes == nil {
return false
}

if interactive {
h := highlight("diff", *changes)
if pager {
if interactive {
h := highlight("diff", *changes)
pageln(h)
} else {
fmt.Println(h)
fmt.Println(*changes)
}
} else {
fmt.Println(*changes)

os.Exit(ExitStatusDiff)
}

return true
return cmd
}

func showCmd() *cobra.Command {
Expand All @@ -152,26 +117,18 @@ func showCmd() *cobra.Command {
},
}
vars := workflowFlags(cmd.Flags())
canRedirect := cmd.Flags().Bool("dangerous-allow-redirect", false, "allow redirecting output to a file or a pipe.")
allowRedirect := cmd.Flags().Bool("dangerous-allow-redirect", false, "allow redirecting output to a file or a pipe.")
cmd.Run = func(cmd *cobra.Command, args []string) {
if !interactive && !*canRedirect {
if !interactive && !*allowRedirect {
fmt.Fprintln(os.Stderr, "Redirection of the output of tk show is discouraged and disabled by default. Run tk show --dangerous-allow-redirect to enable.")
return
}

raw, err := evalDict(args[0])
if err != nil {
log.Fatalln("Evaluating jsonnet:", err)
}

state, err := kube.Reconcile(raw, stringsToRegexps(vars.targets)...)
if err != nil {
log.Fatalln("Reconciling:", err)
}

pretty, err := kube.Fmt(state)
pretty, err := tanka.Show(args[0],
tanka.WithTargets(stringsToRegexps(vars.targets)...),
)
if err != nil {
log.Fatalln("Pretty printing state:", err)
log.Fatalln(err)
}

pageln(pretty)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ require (
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,5 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652 h1:VKvJ/mQ4BgCjZUDggYFxTe0qv9jPMHsZPD4Xt91Y5H4=
gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2 changes: 1 addition & 1 deletion pkg/util/alert.go → pkg/cli/alert.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package util
package cli

import (
"bufio"
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion pkg/jsonnet/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TransitiveImports(filename string) ([]string, error) {
return nil, errors.Wrap(err, "creating Jsonnet AST")
}

imports := make([]string, 0, 0)
imports := make([]string, 0)
err = importRecursive(&imports, vm, node, "main.jsonnet")

return uniqueStringSlice(imports), err
Expand Down
15 changes: 7 additions & 8 deletions pkg/kubernetes/errors.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package kubernetes

import "fmt"
import (
"errors"
"fmt"
)

type ErrorNotFound struct {
name string
Expand All @@ -11,10 +14,6 @@ func (e ErrorNotFound) Error() string {
return fmt.Sprintf(`error from server (NotFound): %s "%s" not found`, e.kind, e.name)
}

type ErrorMissingConfig struct {
Verb string
}

func (e ErrorMissingConfig) Error() string {
return fmt.Sprintf("%s requires additional configuration. Refer to https://tanka.dev/environments for that.", e.Verb)
}
var (
ErrorMissingConfig = errors.New("This operation requires additional configuration. Refer to https://tanka.dev/environments for instructions")
)
Loading

0 comments on commit c5edb8b

Please sign in to comment.