Skip to content

Commit

Permalink
feat(cli): Charttool (#367)
Browse files Browse the repository at this point in the history
* refactor: helmraiser -> helm

Renames the helmraiser package to helm, because its scope is going to be
extended in the following.

It will not only cover helmraiser functionality going on, but also house
declarative chart management code

* refactor: helm.go -> template.go

* feat(cli): Charttool

Adds a new `tk tool charts` command, which enables declarative
management of vendored Helm charts.

As proposed by
https://docs.google.com/document/d/171F0cm_VliMStmHe6oy5pAbuXihr-7Cb5yD-vmwqpP8,
Helm Charts should be vendored by the individual library that consumes
them.

To simplify this process, `tk tool charts` provides a declarative config
file to automate `helm pull`.

The rest of above document will be implemented in subsequent pull requests

* chore: don't ignore cmd/tk folder

wtf happened here

* style: Load -> LoadChartFile
  • Loading branch information
sh0rez authored Sep 1, 2020
1 parent d985b00 commit 735375d
Show file tree
Hide file tree
Showing 11 changed files with 520 additions and 29 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
dist
tk

7 changes: 5 additions & 2 deletions cmd/tk/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ func toolCmd() *cli.Command {
Short: "handy utilities for working with jsonnet",
Use: "tool [command]",
}
cmd.AddCommand(jpathCmd())
cmd.AddCommand(importsCmd())
cmd.AddCommand(
jpathCmd(),
importsCmd(),
chartsCmd(),
)
return cmd
}

Expand Down
126 changes: 126 additions & 0 deletions cmd/tk/toolCharts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package main

import (
"fmt"
"log"
"os"
"path/filepath"

"github.com/go-clix/cli"
"github.com/grafana/tanka/pkg/helm"
"gopkg.in/yaml.v2"
)

func chartsCmd() *cli.Command {
cmd := &cli.Command{
Use: "charts",
Short: "Declarative vendoring of Helm Charts",
}

cmd.AddCommand(
chartsInitCmd(),
chartsAddCmd(),
chartsVendorCmd(),
chartsConfigCmd(),
)

return cmd
}

func chartsVendorCmd() *cli.Command {
cmd := &cli.Command{
Use: "vendor",
Short: "Download Charts to a local folder",
}

cmd.Run = func(cmd *cli.Command, args []string) error {
c, err := loadChartfile()
if err != nil {
return err
}

return c.Vendor()
}

return cmd
}

func chartsAddCmd() *cli.Command {
cmd := &cli.Command{
Use: "add [chart@version] [...]",
Short: "Adds Charts to the chartfile",
}

cmd.Run = func(cmd *cli.Command, args []string) error {
c, err := loadChartfile()
if err != nil {
return err
}

return c.Add(args)
}

return cmd
}

func chartsConfigCmd() *cli.Command {
cmd := &cli.Command{
Use: "config",
Short: "Displays the current manifest",
}

cmd.Run = func(cmd *cli.Command, args []string) error {
c, err := loadChartfile()
if err != nil {
return err
}

data, err := yaml.Marshal(c.Manifest)
if err != nil {
return err
}

fmt.Print(string(data))

return nil
}

return cmd
}

func chartsInitCmd() *cli.Command {
cmd := &cli.Command{
Use: "init",
Short: "Create a new Chartfile",
}

cmd.Run = func(cmd *cli.Command, args []string) error {
wd, err := os.Getwd()
if err != nil {
return err
}

path := filepath.Join(wd, helm.Filename)
if _, err := os.Stat(path); err == nil {
return fmt.Errorf("Chartfile at '%s' already exists. Aborting", path)
}

if _, err := helm.InitChartfile(path); err != nil {
return err
}

log.Printf("Success! New Chartfile created at '%s'", path)
return nil
}

return cmd
}

func loadChartfile() (*helm.Charts, error) {
wd, err := os.Getwd()
if err != nil {
return nil, err
}

return helm.LoadChartfile(wd)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ require (
gopkg.in/yaml.v2 v2.2.8
gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652
k8s.io/apimachinery v0.18.3
sigs.k8s.io/yaml v1.2.0
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,5 @@ sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h
sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
201 changes: 201 additions & 0 deletions pkg/helm/charts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package helm

import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/Masterminds/semver"
"sigs.k8s.io/yaml"
)

// LoadChartfile opens a Chartfile tree
func LoadChartfile(projectRoot string) (*Charts, error) {
// make sure project root is valid
abs, err := filepath.Abs(projectRoot)
if err != nil {
return nil, err
}

// open chartfile
chartfile := filepath.Join(abs, Filename)
data, err := ioutil.ReadFile(chartfile)
if err != nil {
return nil, err
}

// parse it
c := Chartfile{
Version: Version,
Directory: DefaultDir,
}
if err := yaml.UnmarshalStrict(data, &c); err != nil {
return nil, err
}

for i, r := range c.Requires {
if r.Chart == "" {
return nil, fmt.Errorf("requirements[%v]: 'chart' must be set", i)
}
}

// return Charts handle
charts := &Charts{
Manifest: c,
projectRoot: abs,

// default to ExecHelm, but allow injecting from the outside
Helm: ExecHelm{},
}
return charts, nil
}

// Charts exposes the central Chartfile management functions
type Charts struct {
// Manifest are the chartfile.yaml contents. It holds data about the developers intentions
Manifest Chartfile

// projectRoot is the enclosing directory of chartfile.yaml
projectRoot string

// Helm is the helm implementation underneath. ExecHelm is the default, but
// any implementation of the Helm interface may be used
Helm Helm
}

// ChartDir returns the directory pulled charts are saved in
func (c Charts) ChartDir() string {
return filepath.Join(c.projectRoot, c.Manifest.Directory)
}

// ManifestFile returns the full path to the chartfile.yaml
func (c Charts) ManifestFile() string {
return filepath.Join(c.projectRoot, Filename)
}

// Vendor pulls all Charts specified in the manifest into the local charts
// directory. It fetches the repository index before doing so.
func (c Charts) Vendor() error {
dir := c.ChartDir()
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}

log.Println("Syncing Repositories ...")
if err := c.Helm.RepoUpdate(Opts{Repositories: c.Manifest.Repositories}); err != nil {
return err
}

log.Println("Pulling Charts ...")
for _, r := range c.Manifest.Requires {
err := c.Helm.Pull(r.Chart, r.Version.String(), PullOpts{
Destination: dir,
Opts: Opts{Repositories: c.Manifest.Repositories},
})
if err != nil {
return err
}

log.Printf(" %s@%s", r.Chart, r.Version.String())
}

return nil
}

// Add adds every Chart in reqs to the Manifest after validation, and runs
// Vendor afterwards
func (c Charts) Add(reqs []string) error {
log.Printf("Adding %v Charts ...", len(reqs))

skip := func(s string, err error) {
log.Printf(" Skipping %s: %s.", s, err)
}

// parse new charts, append in memory
added := 0
for _, s := range reqs {
r, err := parseReq(s)
if err != nil {
skip(s, err)
continue
}

if c.Manifest.Requires.Has(*r) {
skip(s, fmt.Errorf("already exists"))
continue
}

c.Manifest.Requires = append(c.Manifest.Requires, *r)
added++
log.Println(" OK:", s)
}

// write out
if err := write(c.Manifest, c.ManifestFile()); err != nil {
return err
}

// skipped some? fail then
if added != len(reqs) {
return fmt.Errorf("%v Charts were skipped. Please check above logs for details", len(reqs)-added)
}

// worked fine? vendor it
log.Printf("Added %v Charts to helmfile.yaml. Vendoring ...", added)
return c.Vendor()
}

func InitChartfile(path string) (*Charts, error) {
c := Chartfile{
Version: Version,
Repositories: []Repo{{
Name: "stable",
URL: "https://kubernetes-charts.storage.googleapis.com",
}},
Requires: make(Requirements, 0),
}

if err := write(c, path); err != nil {
return nil, err
}

return LoadChartfile(filepath.Dir(path))
}

// write saves a Chartfile to dest
func write(c Chartfile, dest string) error {
data, err := yaml.Marshal(c)
if err != nil {
return err
}

return ioutil.WriteFile(dest, data, 0644)
}

var chartExp = regexp.MustCompile(`\w+\/\w+@.+`)

// parseReq parses a requirement from a string of the format `repo/name@version`
func parseReq(s string) (*Requirement, error) {
if !chartExp.MatchString(s) {
return nil, fmt.Errorf("not of form 'repo/chart@version'")
}

elems := strings.Split(s, "@")
chart := elems[0]
ver, err := semver.NewVersion(elems[1])
if errors.Is(err, semver.ErrInvalidSemVer) {
return nil, fmt.Errorf("version is invalid")
} else if err != nil {
return nil, fmt.Errorf("version is invalid: %s", err)
}

return &Requirement{
Chart: chart,
Version: *ver,
}, nil
}
Loading

0 comments on commit 735375d

Please sign in to comment.