Skip to content

Commit

Permalink
feat: add template command.
Browse files Browse the repository at this point in the history
fix: store Chart.yaml in memory

Signed-off-by: joyceliu <[email protected]>
  • Loading branch information
joyceliu committed Jan 24, 2024
1 parent 90599b5 commit ebcff94
Show file tree
Hide file tree
Showing 22 changed files with 2,174 additions and 460 deletions.
65 changes: 6 additions & 59 deletions cmd/lint.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,20 @@
package cmd

import (
"io/fs"
"os"
"path/filepath"

"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/values"
"sigs.k8s.io/yaml"

"github.com/kubesphere/ksbuilder/cmd/options"
"github.com/kubesphere/ksbuilder/pkg/extension"
"github.com/kubesphere/ksbuilder/pkg/lint"
)

func lintExtensionCmd() *cobra.Command {
client := action.NewLint()
valueOpts := &values.Options{}
o := options.NewLintOptions()

cmd := &cobra.Command{
Use: "lint PATH [flags]",
Aliases: nil,
SuggestFor: nil,
Args: cobra.MinimumNArgs(1),
Short: "This command takes a path to a chart and runs a series of tests to verify that\n" +
"the chart is well-formed.",
Long: "If the linter encounters things that will cause the chart to fail installation,\n" +
Expand All @@ -33,64 +26,18 @@ func lintExtensionCmd() *cobra.Command {
paths = args
}

if stat, err := os.Stat(paths[0]); err == nil {
if stat.IsDir() {
// update Chart.yaml
metadata, err := extension.LoadMetadata(paths[0])
if err != nil {
return err
}
chartYaml, err := metadata.ToChartYaml()
if err != nil {
return err
}
chartMetadata, err := yaml.Marshal(chartYaml)
if err != nil {
return err
}
// set prefix
data := []byte("# Code generated by ksbuilder. DO NOT EDIT.\n\n")
data = append(data, chartMetadata...)
// set file mode
fileMode := fs.FileMode(0644)
if info, err := os.Stat(filepath.Join(paths[0], "Chart.yaml")); err == nil {
fileMode = info.Mode()
}
if err = os.WriteFile(filepath.Join(paths[0], "Chart.yaml"), data, fileMode); err != nil {
return err
}
}
}

// update chart.yaml

if err := lint.WithHelm(client, valueOpts, paths); err != nil {
if err := extension.WithHelm(o, paths); err != nil {
return err
}

if err := lint.WithBuiltins(paths); err != nil {
if err := extension.WithBuiltins(paths); err != nil {
return err
}

return nil
},
}

addHelmLintFlags(cmd, client, valueOpts)
o.AddFlags(cmd, cmd.Flags())
return cmd
}

func addHelmLintFlags(cmd *cobra.Command, client *action.Lint, v *values.Options) {
// client flags
cmd.Flags().BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
cmd.Flags().BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
cmd.Flags().BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors")

// value flags
cmd.Flags().StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
cmd.Flags().StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
cmd.Flags().StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
cmd.Flags().StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
cmd.Flags().StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)")

}
37 changes: 37 additions & 0 deletions cmd/options/lint_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package options

import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/cli/values"
)

type LintOptions struct {
Client *action.Lint
ValueOpts *values.Options
Settings *cli.EnvSettings
}

func NewLintOptions() *LintOptions {
o := &LintOptions{}
o.Client = action.NewLint()
o.ValueOpts = new(values.Options)
o.Settings = cli.New()
return o
}

func (o *LintOptions) AddFlags(cmd *cobra.Command, f *pflag.FlagSet) {
// client flags
cmd.Flags().BoolVar(&o.Client.Strict, "strict", false, "fail on lint warnings")
cmd.Flags().BoolVar(&o.Client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
cmd.Flags().BoolVar(&o.Client.Quiet, "quiet", false, "print only warnings and errors")

// value flags
cmd.Flags().StringSliceVarP(&o.ValueOpts.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
cmd.Flags().StringArrayVar(&o.ValueOpts.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
cmd.Flags().StringArrayVar(&o.ValueOpts.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
cmd.Flags().StringArrayVar(&o.ValueOpts.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
cmd.Flags().StringArrayVar(&o.ValueOpts.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)")
}
254 changes: 254 additions & 0 deletions cmd/options/template_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package options

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

"github.com/spf13/cobra"
"github.com/spf13/pflag"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/repo"
"k8s.io/client-go/util/homedir"
)

type TemplateOptions struct {
Validate bool
IncludeCrds bool
SkipTests bool
KubeVersion string
ExtraAPIs []string
ShowFiles []string
Client *action.Install
ValueOpts *values.Options
Settings *cli.EnvSettings
}

func NewTemplateOptions() *TemplateOptions {
o := &TemplateOptions{}
o.Client = action.NewInstall(new(action.Configuration))
o.ValueOpts = new(values.Options)
o.Settings = cli.New()
return o
}

func (o *TemplateOptions) AddFlags(cmd *cobra.Command, f *pflag.FlagSet) {
addInstallFlags(cmd, f, o.Client, o.ValueOpts)
f.StringArrayVarP(&o.ShowFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
f.StringVar(&o.Client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
f.BoolVar(&o.Validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install")
f.BoolVar(&o.IncludeCrds, "include-crds", false, "include CRDs in the templated output")
f.BoolVar(&o.SkipTests, "skip-tests", false, "skip tests from templated output")
f.BoolVar(&o.Client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
f.StringVar(&o.KubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
f.StringSliceVarP(&o.ExtraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
f.BoolVar(&o.Client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
bindPostRenderFlag(cmd, &o.Client.PostRenderer)
}

func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
// --dry-run options with expected outcome:
// - Not set means no dry run and server is contacted.
// - Set with no value, a value of client, or a value of true and the server is not contacted
// - Set with a value of false, none, or false and the server is contacted
// The true/false part is meant to reflect some legacy behavior while none is equal to "".
f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
f.Lookup("dry-run").NoOptDefVal = "client"
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout")
f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)")
f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release")
f.StringVar(&client.Description, "description", "", "add a custom description")
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart")
f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema")
f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used")
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present")
f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
f.StringToStringVarP(&client.Labels, "labels", "l", nil, "Labels that would be added to release metadata. Should be divided by comma.")
f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates")
addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions)

err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
requiredArgs := 2
if client.GenerateName {
requiredArgs = 1
}
if len(args) != requiredArgs {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return compVersionFlag(args[requiredArgs-1], toComplete)
})

if err != nil {
log.Fatal(err)
}
}

func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)")
f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line")
}

func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.StringVar(&c.Version, "version", "", "specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used")
f.BoolVar(&c.Verify, "verify", false, "verify the package before using it")
f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification")
f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart")
f.StringVar(&c.Username, "username", "", "chart repository username where to locate the requested chart")
f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart")
f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
f.BoolVar(&c.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
}

const (
outputFlag = "output"
postRenderFlag = "post-renderer"
postRenderArgsFlag = "post-renderer-args"
)

type postRendererOptions struct {
renderer *postrender.PostRenderer
binaryPath string
args []string
}

type postRendererString struct {
options *postRendererOptions
}

func (p *postRendererString) String() string {
return p.options.binaryPath
}

func (p *postRendererString) Type() string {
return "postRendererString"
}

func (p *postRendererString) Set(val string) error {
if val == "" {
return nil
}
p.options.binaryPath = val
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
if err != nil {
return err
}
*p.options.renderer = pr
return nil
}

type postRendererArgsSlice struct {
options *postRendererOptions
}

func (p *postRendererArgsSlice) String() string {
return "[" + strings.Join(p.options.args, ",") + "]"
}

func (p *postRendererArgsSlice) Type() string {
return "postRendererArgsSlice"
}

func (p *postRendererArgsSlice) Set(val string) error {

// a post-renderer defined by a user may accept empty arguments
p.options.args = append(p.options.args, val)

if p.options.binaryPath == "" {
return nil
}
// overwrite if already create PostRenderer by `post-renderer` flags
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
if err != nil {
return err
}
*p.options.renderer = pr
return nil
}

func (p *postRendererArgsSlice) Append(val string) error {
p.options.args = append(p.options.args, val)
return nil
}

func (p *postRendererArgsSlice) Replace(val []string) error {
p.options.args = val
return nil
}

func (p *postRendererArgsSlice) GetSlice() []string {
return p.options.args
}

func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) {
p := &postRendererOptions{varRef, "", []string{}}
cmd.Flags().Var(&postRendererString{p}, postRenderFlag, "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path")
cmd.Flags().Var(&postRendererArgsSlice{p}, postRenderArgsFlag, "an argument to the post-renderer (can specify multiple)")
}

// defaultKeyring returns the expanded path to the default keyring.
func defaultKeyring() string {
if v, ok := os.LookupEnv("GNUPGHOME"); ok {
return filepath.Join(v, "pubring.gpg")
}
return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg")
}

var settings = cli.New()

func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellCompDirective) {
chartInfo := strings.Split(chartRef, "/")
if len(chartInfo) != 2 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

repoName := chartInfo[0]
chartName := chartInfo[1]

path := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))

var versions []string
if indexFile, err := repo.LoadIndexFile(path); err == nil {
for _, details := range indexFile.Entries[chartName] {
appVersion := details.Metadata.AppVersion
appVersionDesc := ""
if appVersion != "" {
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
}
created := details.Created.Format("January 2, 2006")
createdDesc := ""
if created != "" {
createdDesc = fmt.Sprintf("Created: %s ", created)
}
deprecated := ""
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Metadata.Version, appVersionDesc, createdDesc, deprecated))
}
}

return versions, cobra.ShellCompDirectiveNoFileComp
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func NewRootCmd(version string) *cobra.Command {
cmd.AddCommand(unpublishExtensionCmd())
cmd.AddCommand(validateExtensionCmd())
cmd.AddCommand(lintExtensionCmd())
cmd.AddCommand(templateExtensionCmd())

return cmd
}
Expand Down
Loading

0 comments on commit ebcff94

Please sign in to comment.