Skip to content

Commit 6ed28c0

Browse files
authored
Merge pull request #79 from littleBlackHouse/lint
feat: add template command.
2 parents 6a8fca9 + ebcff94 commit 6ed28c0

22 files changed

Lines changed: 2174 additions & 460 deletions

cmd/lint.go

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
11
package cmd
22

33
import (
4-
"io/fs"
5-
"os"
6-
"path/filepath"
7-
84
"github.com/spf13/cobra"
9-
"helm.sh/helm/v3/pkg/action"
10-
"helm.sh/helm/v3/pkg/cli/values"
11-
"sigs.k8s.io/yaml"
125

6+
"github.com/kubesphere/ksbuilder/cmd/options"
137
"github.com/kubesphere/ksbuilder/pkg/extension"
14-
"github.com/kubesphere/ksbuilder/pkg/lint"
158
)
169

1710
func lintExtensionCmd() *cobra.Command {
18-
client := action.NewLint()
19-
valueOpts := &values.Options{}
11+
o := options.NewLintOptions()
2012

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

36-
if stat, err := os.Stat(paths[0]); err == nil {
37-
if stat.IsDir() {
38-
// update Chart.yaml
39-
metadata, err := extension.LoadMetadata(paths[0])
40-
if err != nil {
41-
return err
42-
}
43-
chartYaml, err := metadata.ToChartYaml()
44-
if err != nil {
45-
return err
46-
}
47-
chartMetadata, err := yaml.Marshal(chartYaml)
48-
if err != nil {
49-
return err
50-
}
51-
// set prefix
52-
data := []byte("# Code generated by ksbuilder. DO NOT EDIT.\n\n")
53-
data = append(data, chartMetadata...)
54-
// set file mode
55-
fileMode := fs.FileMode(0644)
56-
if info, err := os.Stat(filepath.Join(paths[0], "Chart.yaml")); err == nil {
57-
fileMode = info.Mode()
58-
}
59-
if err = os.WriteFile(filepath.Join(paths[0], "Chart.yaml"), data, fileMode); err != nil {
60-
return err
61-
}
62-
}
63-
}
64-
65-
// update chart.yaml
66-
67-
if err := lint.WithHelm(client, valueOpts, paths); err != nil {
29+
if err := extension.WithHelm(o, paths); err != nil {
6830
return err
6931
}
7032

71-
if err := lint.WithBuiltins(paths); err != nil {
33+
if err := extension.WithBuiltins(paths); err != nil {
7234
return err
7335
}
7436

7537
return nil
7638
},
7739
}
7840

79-
addHelmLintFlags(cmd, client, valueOpts)
41+
o.AddFlags(cmd, cmd.Flags())
8042
return cmd
8143
}
82-
83-
func addHelmLintFlags(cmd *cobra.Command, client *action.Lint, v *values.Options) {
84-
// client flags
85-
cmd.Flags().BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
86-
cmd.Flags().BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
87-
cmd.Flags().BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors")
88-
89-
// value flags
90-
cmd.Flags().StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
91-
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)")
92-
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)")
93-
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)")
94-
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)")
95-
96-
}

cmd/options/lint_options.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package options
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/spf13/pflag"
6+
"helm.sh/helm/v3/pkg/action"
7+
"helm.sh/helm/v3/pkg/cli"
8+
"helm.sh/helm/v3/pkg/cli/values"
9+
)
10+
11+
type LintOptions struct {
12+
Client *action.Lint
13+
ValueOpts *values.Options
14+
Settings *cli.EnvSettings
15+
}
16+
17+
func NewLintOptions() *LintOptions {
18+
o := &LintOptions{}
19+
o.Client = action.NewLint()
20+
o.ValueOpts = new(values.Options)
21+
o.Settings = cli.New()
22+
return o
23+
}
24+
25+
func (o *LintOptions) AddFlags(cmd *cobra.Command, f *pflag.FlagSet) {
26+
// client flags
27+
cmd.Flags().BoolVar(&o.Client.Strict, "strict", false, "fail on lint warnings")
28+
cmd.Flags().BoolVar(&o.Client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
29+
cmd.Flags().BoolVar(&o.Client.Quiet, "quiet", false, "print only warnings and errors")
30+
31+
// value flags
32+
cmd.Flags().StringSliceVarP(&o.ValueOpts.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
33+
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)")
34+
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)")
35+
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)")
36+
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)")
37+
}

cmd/options/template_options.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package options
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"time"
10+
11+
"github.com/spf13/cobra"
12+
"github.com/spf13/pflag"
13+
"helm.sh/helm/v3/pkg/action"
14+
"helm.sh/helm/v3/pkg/cli"
15+
"helm.sh/helm/v3/pkg/cli/values"
16+
"helm.sh/helm/v3/pkg/helmpath"
17+
"helm.sh/helm/v3/pkg/postrender"
18+
"helm.sh/helm/v3/pkg/repo"
19+
"k8s.io/client-go/util/homedir"
20+
)
21+
22+
type TemplateOptions struct {
23+
Validate bool
24+
IncludeCrds bool
25+
SkipTests bool
26+
KubeVersion string
27+
ExtraAPIs []string
28+
ShowFiles []string
29+
Client *action.Install
30+
ValueOpts *values.Options
31+
Settings *cli.EnvSettings
32+
}
33+
34+
func NewTemplateOptions() *TemplateOptions {
35+
o := &TemplateOptions{}
36+
o.Client = action.NewInstall(new(action.Configuration))
37+
o.ValueOpts = new(values.Options)
38+
o.Settings = cli.New()
39+
return o
40+
}
41+
42+
func (o *TemplateOptions) AddFlags(cmd *cobra.Command, f *pflag.FlagSet) {
43+
addInstallFlags(cmd, f, o.Client, o.ValueOpts)
44+
f.StringArrayVarP(&o.ShowFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
45+
f.StringVar(&o.Client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
46+
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")
47+
f.BoolVar(&o.IncludeCrds, "include-crds", false, "include CRDs in the templated output")
48+
f.BoolVar(&o.SkipTests, "skip-tests", false, "skip tests from templated output")
49+
f.BoolVar(&o.Client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
50+
f.StringVar(&o.KubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
51+
f.StringSliceVarP(&o.ExtraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
52+
f.BoolVar(&o.Client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
53+
bindPostRenderFlag(cmd, &o.Client.PostRenderer)
54+
}
55+
56+
func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
57+
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
58+
// --dry-run options with expected outcome:
59+
// - Not set means no dry run and server is contacted.
60+
// - Set with no value, a value of client, or a value of true and the server is not contacted
61+
// - Set with a value of false, none, or false and the server is contacted
62+
// The true/false part is meant to reflect some legacy behavior while none is equal to "".
63+
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.")
64+
f.Lookup("dry-run").NoOptDefVal = "client"
65+
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
66+
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
67+
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")
68+
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
69+
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")
70+
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")
71+
f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)")
72+
f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release")
73+
f.StringVar(&client.Description, "description", "", "add a custom description")
74+
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
75+
f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart")
76+
f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema")
77+
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")
78+
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present")
79+
f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
80+
f.StringToStringVarP(&client.Labels, "labels", "l", nil, "Labels that would be added to release metadata. Should be divided by comma.")
81+
f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates")
82+
addValueOptionsFlags(f, valueOpts)
83+
addChartPathOptionsFlags(f, &client.ChartPathOptions)
84+
85+
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
86+
requiredArgs := 2
87+
if client.GenerateName {
88+
requiredArgs = 1
89+
}
90+
if len(args) != requiredArgs {
91+
return nil, cobra.ShellCompDirectiveNoFileComp
92+
}
93+
return compVersionFlag(args[requiredArgs-1], toComplete)
94+
})
95+
96+
if err != nil {
97+
log.Fatal(err)
98+
}
99+
}
100+
101+
func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
102+
f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
103+
f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
104+
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)")
105+
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)")
106+
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)")
107+
f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line")
108+
}
109+
110+
func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
111+
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")
112+
f.BoolVar(&c.Verify, "verify", false, "verify the package before using it")
113+
f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification")
114+
f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart")
115+
f.StringVar(&c.Username, "username", "", "chart repository username where to locate the requested chart")
116+
f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart")
117+
f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
118+
f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file")
119+
f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
120+
f.BoolVar(&c.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download")
121+
f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
122+
f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains")
123+
}
124+
125+
const (
126+
outputFlag = "output"
127+
postRenderFlag = "post-renderer"
128+
postRenderArgsFlag = "post-renderer-args"
129+
)
130+
131+
type postRendererOptions struct {
132+
renderer *postrender.PostRenderer
133+
binaryPath string
134+
args []string
135+
}
136+
137+
type postRendererString struct {
138+
options *postRendererOptions
139+
}
140+
141+
func (p *postRendererString) String() string {
142+
return p.options.binaryPath
143+
}
144+
145+
func (p *postRendererString) Type() string {
146+
return "postRendererString"
147+
}
148+
149+
func (p *postRendererString) Set(val string) error {
150+
if val == "" {
151+
return nil
152+
}
153+
p.options.binaryPath = val
154+
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
155+
if err != nil {
156+
return err
157+
}
158+
*p.options.renderer = pr
159+
return nil
160+
}
161+
162+
type postRendererArgsSlice struct {
163+
options *postRendererOptions
164+
}
165+
166+
func (p *postRendererArgsSlice) String() string {
167+
return "[" + strings.Join(p.options.args, ",") + "]"
168+
}
169+
170+
func (p *postRendererArgsSlice) Type() string {
171+
return "postRendererArgsSlice"
172+
}
173+
174+
func (p *postRendererArgsSlice) Set(val string) error {
175+
176+
// a post-renderer defined by a user may accept empty arguments
177+
p.options.args = append(p.options.args, val)
178+
179+
if p.options.binaryPath == "" {
180+
return nil
181+
}
182+
// overwrite if already create PostRenderer by `post-renderer` flags
183+
pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...)
184+
if err != nil {
185+
return err
186+
}
187+
*p.options.renderer = pr
188+
return nil
189+
}
190+
191+
func (p *postRendererArgsSlice) Append(val string) error {
192+
p.options.args = append(p.options.args, val)
193+
return nil
194+
}
195+
196+
func (p *postRendererArgsSlice) Replace(val []string) error {
197+
p.options.args = val
198+
return nil
199+
}
200+
201+
func (p *postRendererArgsSlice) GetSlice() []string {
202+
return p.options.args
203+
}
204+
205+
func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) {
206+
p := &postRendererOptions{varRef, "", []string{}}
207+
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")
208+
cmd.Flags().Var(&postRendererArgsSlice{p}, postRenderArgsFlag, "an argument to the post-renderer (can specify multiple)")
209+
}
210+
211+
// defaultKeyring returns the expanded path to the default keyring.
212+
func defaultKeyring() string {
213+
if v, ok := os.LookupEnv("GNUPGHOME"); ok {
214+
return filepath.Join(v, "pubring.gpg")
215+
}
216+
return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg")
217+
}
218+
219+
var settings = cli.New()
220+
221+
func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellCompDirective) {
222+
chartInfo := strings.Split(chartRef, "/")
223+
if len(chartInfo) != 2 {
224+
return nil, cobra.ShellCompDirectiveNoFileComp
225+
}
226+
227+
repoName := chartInfo[0]
228+
chartName := chartInfo[1]
229+
230+
path := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))
231+
232+
var versions []string
233+
if indexFile, err := repo.LoadIndexFile(path); err == nil {
234+
for _, details := range indexFile.Entries[chartName] {
235+
appVersion := details.Metadata.AppVersion
236+
appVersionDesc := ""
237+
if appVersion != "" {
238+
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
239+
}
240+
created := details.Created.Format("January 2, 2006")
241+
createdDesc := ""
242+
if created != "" {
243+
createdDesc = fmt.Sprintf("Created: %s ", created)
244+
}
245+
deprecated := ""
246+
if details.Metadata.Deprecated {
247+
deprecated = "(deprecated)"
248+
}
249+
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Metadata.Version, appVersionDesc, createdDesc, deprecated))
250+
}
251+
}
252+
253+
return versions, cobra.ShellCompDirectiveNoFileComp
254+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func NewRootCmd(version string) *cobra.Command {
2525
cmd.AddCommand(unpublishExtensionCmd())
2626
cmd.AddCommand(validateExtensionCmd())
2727
cmd.AddCommand(lintExtensionCmd())
28+
cmd.AddCommand(templateExtensionCmd())
2829

2930
return cmd
3031
}

0 commit comments

Comments
 (0)