-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
``` | ||
|
||
On OpenShift you can ``apb push`` to build and push the service bundle into your | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typically you only need one back tick to format code inline. |
||
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. |
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
"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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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 namedredis-1.1.12-apb
? Or even dropping the version andredis-apb
. I also know it might be dumb to create a directory of 2 files...There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.