Skip to content
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(cli): Charttool #367

Merged
merged 5 commits into from
Sep 1, 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
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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we colourise this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not easily. The only yaml highlighter library I am aware of (alecthomas/chroma) doesn't handle yaml well, especially not on light schemed terminals.

We actually removed it from the project for that exact reason some time ago (were coloring diff using it)

We could look into writing a custom yaml highlighter some day


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))
sh0rez marked this conversation as resolved.
Show resolved Hide resolved

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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want this? Being deprecated soon?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it's the primary source of truth. Soon is not yet deprecated :)

We will remove as soon as deprecated

}},
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