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: add template command. #79

Merged
merged 1 commit into from
Jan 25, 2024
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
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
Loading