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

re-adds source and docs for review #4

Merged
merged 3 commits into from
Apr 3, 2018
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# helm2bundle

Inspects a helm chart and outputs an apb.yml file and Dockerfile based on
values in the chart.

## Status

This is pre-release experimental software.


## Usage

```
$ helm2bundle redis-1.1.12.tgz
$ ls
apb.yml Dockerfile redis-1.1.12.tgz

Choose a reason for hiding this comment

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

What do you think about following the same format as apb init and creating a directory named redis-1.1.12-apb? Or even dropping the version and redis-apb. I also know it might be dumb to create a directory of 2 files...

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting idea. I think the main reason we wouldn't do that is that the image needs the chart file itself. All three of the files you see here have to be used while building the image. apb init is a bit different since it is starting from 0 assets.

Choose a reason for hiding this comment

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

+1, doesn't really make sense the more I think about it.

```

On OpenShift you can ``apb push`` to build and push the service bundle into your
Copy link

Choose a reason for hiding this comment

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

typically you only need one back tick to format code inline. like this, no need to change in this document but could save you 2 extra characters in future documents. Imagine the time you could save in a lifetime :)

cluster's registry.

On plain Kubernetes, you can ``apb build`` and then tag and push to a registry that
your broker is configured to access.
304 changes: 304 additions & 0 deletions helm2bundle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
package main

import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"text/template"
Copy link

Choose a reason for hiding this comment

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

goimport tool will reorder the imports to be ordered as stdlib first and alphabetized. Then third party, alphabetized. I use vim-go with goimports which handles this for me.

import (
    "archive/tar"
    "compress/gzip"
    "errors"
    "fmt"
    "io"
    "io/ioutil"
    "os"
    "path"
    "text/template"
    
    "github.com/spf13/cobra"
    "gopkg.in/yaml.v2"
)


"github.com/spf13/cobra"
"gopkg.in/yaml.v2"
)

const dockerfileTemplate string = `FROM ansibleplaybookbundle/helm-bundle-base

LABEL "com.redhat.apb.spec"=\
""

COPY {{.TarfileName}} /opt/chart.tgz

ENTRYPOINT ["entrypoint.sh"]
`

const apbYml string = "apb.yml"
const dockerfile string = "Dockerfile"

// APB represents an apb.yml file
type APB struct {
Version string `yaml:"version"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Bindable bool `yaml:"bindable"`
Async string `yaml:"async"`
Metadata map[string]string `yaml:"metadata"`
Plans []Plan `yaml:"plans"`
}

// Plan represents a Plan within an APB
type Plan struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Free bool `yaml:"free"`
Metadata map[string]interface{} `yaml:"metadata"`
Parameters []Parameter `yaml:"parameters"`
}

// Parameter represents a Parameter within a Plan
type Parameter struct {
Name string `yaml:"name"`
Title string `yaml:"title"`
Type string `yaml:"type"`
DisplayType string `yaml:"display_type"`
Default string `yaml:"default"`
}

// NewAPB returns a pointer to a new APB that has been populated with the
// passed-in data.
func NewAPB(v TarValues) *APB {
parameter := Parameter{
Name: "values",
Title: "Values",
Type: "string",
DisplayType: "textarea",
Default: v.Values,
}
plan := Plan{
Name: "default",
Description: fmt.Sprintf("Deploys helm chart %s", v.Name),
Free: true,
Metadata: make(map[string]interface{}),
Parameters: []Parameter{parameter},
}
apb := APB{
Version: "1.0",
Name: fmt.Sprintf("%s-apb", v.Name),
Description: v.Description,
Bindable: false,
Async: "optional",
Metadata: map[string]string{
"displayName": fmt.Sprintf("%s (helm bundle)", v.Name),
"imageUrl": v.Icon,
},
Plans: []Plan{plan},
}
return &apb
}

// TarValues holds data that will be used to create the Dockerfile and apb.yml
type TarValues struct {
Name string
Description string
Icon string
TarfileName string
Values string // the entire contents of the chart's values.yaml file
}

// Chart holds data that is parsed from a helm chart's Chart.yaml file.
type Chart struct {
Description string
Name string
Icon string
}

func main() {
// forceArg is true when the user specifies --force, and it indicates that
// it is ok to replace existing files.
var forceArg bool

var rootCmd = &cobra.Command{
Use: "helm2bundle CHARTFILE",
Short: "Packages a helm chart as a Service Bundle",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
Copy link

Choose a reason for hiding this comment

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

the func here is clever but makes the code harder to read since it is longer than a few lines. Not a blocker but I would've had a separate function for it. If we add new parameters I can see this main function becoming quite large.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. I started just from the cobra examples, and it grew from there. I'll factor most of it out.

run(forceArg, args[0])
},
}

rootCmd.PersistentFlags().BoolVarP(&forceArg, "force", "f", false, "force overwrite of existing files")

err := rootCmd.Execute()
if err != nil {
fmt.Println(err.Error())
fmt.Println("could not execute command")
os.Exit(1)
}
}

// run does all of the real work. `force` indicates if existing files should be
// overwritten, and `filename` is the name of the chart file in the working
// directory.
func run(force bool, filename string) {
if force == false {
// fail if one of the files already exists
exists, err := fileExists()
if err != nil {
fmt.Println(err.Error())
fmt.Println("could not get values from helm chart")
os.Exit(1)
}
if exists {
fmt.Printf("use --force to overwrite existing %s and/or %s\n", dockerfile, apbYml)
os.Exit(1)
}
}

values, err := getTarValues(filename)
if err != nil {
fmt.Println(err.Error())
fmt.Println("could not get values from helm chart")
os.Exit(1)
}

err = writeApbYaml(values)
if err != nil {
fmt.Println(err.Error())
fmt.Println("could not render template")
os.Exit(1)
}
err = writeDockerfile(values)
if err != nil {
fmt.Println(err.Error())
fmt.Println("could not render template")
os.Exit(1)
}
}

// fileExists returns true if either apb.yml or Dockerfile exists in the
// working directory, else false
func fileExists() (bool, error) {
for _, filename := range []string{apbYml, dockerfile} {
_, err := os.Stat(filename)
if err == nil {
// file exists
return true, nil
}
if !os.IsNotExist(err) {
// error determining if file exists
return false, err
}
}
// neither file exists
return false, nil
}

// writeApbYaml creates a new file named "apb.yml" in the current working
// directory that can be used to build a service bundle.
func writeApbYaml(v TarValues) error {
apb := NewAPB(v)
data, err := yaml.Marshal(apb)
if err != nil {
return err
}

f, err := os.Create(apbYml)
if err != nil {
return err
}
defer f.Close()

_, err = f.Write(data)
return err
}

// writeDockerfile creates a new file named "Dockerfile" in the current working
// directory that can be used to build a service bundle.
func writeDockerfile(v TarValues) error {
t, err := template.New(dockerfile).Parse(dockerfileTemplate)
if err != nil {
return err
}

f, err := os.Create(dockerfile)
if err != nil {
return err
}
defer f.Close()

return t.Execute(f, v)
}

// getTarValues opens the helm chart tarball to 1) retrieve Chart.yaml so it can
// be parsed, and 2) retrieve the entire contents of values.yaml.
func getTarValues(filename string) (TarValues, error) {
Copy link

Choose a reason for hiding this comment

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

nice looks good.

file, err := os.Open(filename)
if err != nil {
return TarValues{}, err
}
defer file.Close()

uncompressed, err := gzip.NewReader(file)
if err != nil {
return TarValues{}, err
}

tr := tar.NewReader(uncompressed)
var chart Chart
var values string
for {
hdr, err := tr.Next()
if err == io.EOF {
return TarValues{}, errors.New("Chart.yaml not found in archive")
}
if err != nil {
return TarValues{}, err
}

chartMatch, err := path.Match("*/Chart.yaml", hdr.Name)
if err != nil {
return TarValues{}, err
}
valuesMatch, err := path.Match("*/values.yaml", hdr.Name)
if err != nil {
return TarValues{}, err
}
if chartMatch {
chart, err = parseChart(tr)
if err != nil {
return TarValues{}, err
}
}
if valuesMatch {
data, err := ioutil.ReadAll(tr)
if err != nil {
return TarValues{}, err
}
values = string(data)
}
if len(values) > 0 && len(chart.Name) > 0 {
break
}
}
if len(values) > 0 && len(chart.Name) > 0 {
return TarValues{
Name: chart.Name,
Description: chart.Description,
Icon: chart.Icon,
TarfileName: filename,
Values: values,
}, nil
}
return TarValues{}, errors.New("Could not find both Chart.yaml and values.yaml")
}

// parseChart parses the Chart.yaml file for data that is needed when creating
// a service bundle.
func parseChart(source io.Reader) (Chart, error) {
c := Chart{}

data, err := ioutil.ReadAll(source)
if err != nil {
return c, err
}

err = yaml.Unmarshal(data, &c)
if err != nil {
return c, err
}

return c, nil
}