diff --git a/.github/workflows/goreleaser-cd.yml b/.github/workflows/goreleaser-cd.yml index 3edb26bb..23168009 100644 --- a/.github/workflows/goreleaser-cd.yml +++ b/.github/workflows/goreleaser-cd.yml @@ -273,6 +273,8 @@ jobs: run: "packer init ./packer/" - name: Run `packer validate` id: validate + env: + PKR_VAR_k8s_cli_version: ${{ github.ref_name}} run: "packer validate ./packer/" - name: Run `packer build` id: build diff --git a/cmd/plural/deploy.go b/cmd/plural/deploy.go index 8bd9fe58..c1ab537c 100644 --- a/cmd/plural/deploy.go +++ b/cmd/plural/deploy.go @@ -155,6 +155,7 @@ func (p *Plural) doBuild(installation *api.Installation, force bool) error { workspace.PrintLinks() + appReadme(repoName, false) // nolint:errcheck return err } diff --git a/cmd/plural/push.go b/cmd/plural/push.go index c56c3683..833a3446 100644 --- a/cmd/plural/push.go +++ b/cmd/plural/push.go @@ -103,6 +103,7 @@ func handleHelmTemplate(c *cli.Context) error { _ = os.Remove(name) }(f.Name()) + name := "default" namespace := "default" actionConfig, err := helm.GetActionConfig(namespace) if err != nil { @@ -112,7 +113,7 @@ func handleHelmTemplate(c *cli.Context) error { if err != nil { return err } - res, err := helm.Template(actionConfig, c.Args().Get(0), namespace, "./", false, false, values) + res, err := helm.Template(actionConfig, name, namespace, c.Args().Get(0), false, false, values) if err != nil { return err } diff --git a/cmd/plural/workspace.go b/cmd/plural/workspace.go index 22e258d7..4afda525 100644 --- a/cmd/plural/workspace.go +++ b/cmd/plural/workspace.go @@ -10,7 +10,9 @@ import ( "github.com/pluralsh/plural/pkg/helm" "github.com/pluralsh/plural/pkg/provider" + "github.com/pluralsh/plural/pkg/scaffold" "github.com/pluralsh/plural/pkg/utils" + "github.com/pluralsh/plural/pkg/utils/git" "github.com/pluralsh/plural/pkg/wkspace" ) @@ -21,6 +23,18 @@ func (p *Plural) workspaceCommands() []cli.Command { Usage: "generates kubernetes credentials for this subworkspace", Action: latestVersion(kubeInit), }, + { + Name: "readme", + Usage: "generate chart readme for an app", + ArgsUsage: "NAME", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "dry-run", + Usage: "output to stdout instead of to a file", + }, + }, + Action: latestVersion(func(c *cli.Context) error { return appReadme(c.Args().Get(0), c.Bool("dry-run")) }), + }, { Name: "helm", Usage: "upgrade/installs the helm chart for this subworkspace", @@ -204,3 +218,13 @@ func (p *Plural) mapkubeapis(c *cli.Context) error { return minimal.MapKubeApis() } + +func appReadme(name string, dryRun bool) error { + repoRoot, err := git.Root() + if err != nil { + return err + } + + dir := filepath.Join(repoRoot, name, "helm", name) + return scaffold.Readme(dir, dryRun) +} diff --git a/go.mod b/go.mod index ae45078c..8cde289d 100644 --- a/go.mod +++ b/go.mod @@ -26,9 +26,6 @@ require ( github.com/briandowns/spinner v1.23.0 github.com/buger/goterm v1.0.4 github.com/cert-manager/cert-manager v1.10.0 - github.com/charmbracelet/bubbles v0.13.0 - github.com/charmbracelet/bubbletea v0.21.0 - github.com/charmbracelet/lipgloss v0.5.0 github.com/chartmuseum/helm-push v0.10.3 github.com/coreos/go-semver v0.3.0 github.com/databus23/helm-diff/v3 v3.6.0 @@ -51,7 +48,7 @@ require ( github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.5.0 - github.com/muesli/reflow v0.3.0 + github.com/norwoodj/helm-docs v1.11.2 github.com/olekukonko/tablewriter v0.0.5 github.com/packethost/packngo v0.29.0 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 @@ -64,6 +61,7 @@ require ( github.com/rivo/tview v0.0.0-20230615085408-bb9595ee0f4d github.com/rodaine/hclencoder v0.0.1 github.com/samber/lo v1.38.1 + github.com/spf13/viper v1.15.0 github.com/thoas/go-funk v0.9.2 github.com/urfave/cli v1.22.14 github.com/wailsapp/wails/v2 v2.4.1 @@ -143,6 +141,9 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff/v4 v4.2.0 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect + github.com/charmbracelet/bubbles v0.13.0 // indirect + github.com/charmbracelet/bubbletea v0.21.0 // indirect + github.com/charmbracelet/lipgloss v0.5.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cjlapao/common-go v0.0.39 // indirect github.com/cloudflare/cfssl v1.6.3 // indirect @@ -212,6 +213,7 @@ require ( github.com/miekg/dns v1.1.50 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect @@ -227,7 +229,6 @@ require ( github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/viper v1.15.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect @@ -379,7 +380,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-ieproxy v0.0.1 // indirect - github.com/mattn/go-isatty v0.0.19 + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect @@ -412,7 +413,7 @@ require ( github.com/schollz/progressbar/v3 v3.8.6 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.3 github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -456,6 +457,7 @@ require ( ) replace ( + github.com/norwoodj/helm-docs v1.11.2 => github.com/pluralsh/helm-docs v1.11.3-0.20230914190909-3fe18acd95d7 go.etcd.io/etcd/pkg/v3 => go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 k8s.io/cli-runtime => k8s.io/cli-runtime v0.26.3 k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20230109183929-3758b55a6596 diff --git a/go.sum b/go.sum index f6968225..672fe2f3 100644 --- a/go.sum +++ b/go.sum @@ -171,6 +171,7 @@ github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYr github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= @@ -1392,6 +1393,8 @@ github.com/pluralsh/controller-reconcile-helper v0.0.4 h1:1o+7qYSyoeqKFjx+WgQTxD github.com/pluralsh/controller-reconcile-helper v0.0.4/go.mod h1:AfY0gtteD6veBjmB6jiRx/aR4yevEf6K0M13/pGan/s= github.com/pluralsh/gqlclient v1.10.0 h1:ccYB+A0JbPYkEeVzdfajd29l65N6x/buSKPMMxM8OIA= github.com/pluralsh/gqlclient v1.10.0/go.mod h1:qSXKUlio1F2DRPy8el4oFYsmpKbkUYspgPB87T4it5I= +github.com/pluralsh/helm-docs v1.11.3-0.20230914190909-3fe18acd95d7 h1:tfxqRM5zNBXvKXt7TMN4z8PuNUpZ0TbaFT0WUeXSlPY= +github.com/pluralsh/helm-docs v1.11.3-0.20230914190909-3fe18acd95d7/go.mod h1:rLqec59NO7YF57Rq9VlubQHMp7wcRTJhzpkcgs4lOG4= github.com/pluralsh/oauth v0.9.2 h1:tM9hBK4tCnJUeCOgX0ctxBBCS3hiCDPoxkJLODtedmQ= github.com/pluralsh/oauth v0.9.2/go.mod h1:aTUw/75rzcsbvW+/TLvWtHVDXFIdtFrDtUncOq9vHyM= github.com/pluralsh/plural-operator v0.5.5 h1:57GxniNjUa3hpHgvFr9oDonFgvDUC8XDD5B0e7Xduzk= @@ -2127,6 +2130,7 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2520,6 +2524,7 @@ k8s.io/component-base v0.26.1 h1:4ahudpeQXHZL5kko+iDHqLj/FSGAEUnSVO0EBbgDd+4= k8s.io/component-base v0.26.1/go.mod h1:VHrLR0b58oC035w6YQiBSbtsf0ThuSwXP+p5dD/kAWU= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/helm v2.14.3+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/helm v2.17.0+incompatible h1:Bpn6o1wKLYqKM3+Osh8e+1/K2g/GsQJ4F4yNF2+deao= k8s.io/helm v2.17.0+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= diff --git a/pkg/scaffold/creator.go b/pkg/scaffold/creator.go index 12d12316..ff6c4ee2 100644 --- a/pkg/scaffold/creator.go +++ b/pkg/scaffold/creator.go @@ -8,6 +8,7 @@ import ( "github.com/pluralsh/plural/pkg/api" "github.com/pluralsh/plural/pkg/utils" "github.com/pluralsh/plural/pkg/utils/pathing" + "helm.sh/helm/v3/pkg/chartutil" ) var categories = []string{ @@ -77,7 +78,7 @@ func ApplicationScaffold(client api.Client) error { return err } - if err := utils.Exec("helm", "create", app); err != nil { + if err := createHelm(app); err != nil { return err } @@ -93,3 +94,9 @@ func ApplicationScaffold(client api.Client) error { return nil } + +func createHelm(name string) error { + chartname := filepath.Base(name) + _, err := chartutil.Create(chartname, filepath.Dir(name)) + return err +} diff --git a/pkg/scaffold/readme.go b/pkg/scaffold/readme.go new file mode 100644 index 00000000..e16ede9a --- /dev/null +++ b/pkg/scaffold/readme.go @@ -0,0 +1,186 @@ +package scaffold + +import ( + "fmt" + "os" + "path" + "path/filepath" + "reflect" + "regexp" + "runtime" + "strings" + "sync" + + log "github.com/sirupsen/logrus" + + "github.com/norwoodj/helm-docs/pkg/document" + "github.com/norwoodj/helm-docs/pkg/helm" + "github.com/spf13/viper" +) + +// parallelProcessIterable runs the visitFn function on each element of the iterable, using +// parallelism number of worker goroutines. The iterable may be a slice or a map. In the case of a +// map, the argument passed to visitFn will be the key. +func parallelProcessIterable(iterable interface{}, parallelism int, visitFn func(elem interface{})) { + workChan := make(chan interface{}) + + wg := &sync.WaitGroup{} + wg.Add(parallelism) + + for i := 0; i < parallelism; i++ { + go func() { + defer wg.Done() + for elem := range workChan { + visitFn(elem) + } + }() + } + + iterableValue := reflect.ValueOf(iterable) + + if iterableValue.Kind() == reflect.Map { + for _, key := range iterableValue.MapKeys() { + workChan <- key.Interface() + } + } else { + sliceLen := iterableValue.Len() + for i := 0; i < sliceLen; i++ { + workChan <- iterableValue.Index(i).Interface() + } + } + + close(workChan) + wg.Wait() +} + +func getDocumentationParsingConfigFromArgs() (helm.ChartValuesDocumentationParsingConfig, error) { + var regexps []*regexp.Regexp + regexpStrings := []string{".*service\\.type", ".*image\\.repository", ".*image\\.tag"} + for _, item := range regexpStrings { + regex, err := regexp.Compile(item) + if err != nil { + return helm.ChartValuesDocumentationParsingConfig{}, err + } + regexps = append(regexps, regex) + } + return helm.ChartValuesDocumentationParsingConfig{ + StrictMode: false, + AllowedMissingValuePaths: []string{}, + AllowedMissingValueRegexps: regexps, + }, nil +} + +func readDocumentationInfoByChartPath(chartSearchRoot string, parallelism int) (map[string]helm.ChartDocumentationInfo, error) { + var fullChartSearchRoot string + + if path.IsAbs(chartSearchRoot) { + fullChartSearchRoot = chartSearchRoot + } else { + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("error getting working directory: %w", err) + } + + fullChartSearchRoot = filepath.Join(cwd, chartSearchRoot) + } + + chartDirs, err := helm.FindChartDirectories(fullChartSearchRoot) + if err != nil { + return nil, fmt.Errorf("error finding chart directories: %w", err) + } + + log.Infof("Found Chart directories [%s]", strings.Join(chartDirs, ", ")) + + templateFiles := []string{"README.md.gotmpl"} + log.Debugf("Rendering from optional template files [%s]", strings.Join(templateFiles, ", ")) + + documentationInfoByChartPath := make(map[string]helm.ChartDocumentationInfo, len(chartDirs)) + documentationInfoByChartPathMu := &sync.Mutex{} + documentationParsingConfig, err := getDocumentationParsingConfigFromArgs() + if err != nil { + return nil, fmt.Errorf("error parsing the linting config%w", err) + } + + parallelProcessIterable(chartDirs, parallelism, func(elem interface{}) { + chartDir := elem.(string) + info, err := helm.ParseChartInformation(filepath.Join(chartSearchRoot, chartDir), documentationParsingConfig) + if err != nil { + log.Warnf("Error parsing information for chart %s, skipping: %s", chartDir, err) + return + } + documentationInfoByChartPathMu.Lock() + documentationInfoByChartPath[info.ChartDirectory] = info + documentationInfoByChartPathMu.Unlock() + }) + + return documentationInfoByChartPath, nil +} + +func getChartToGenerate(documentationInfoByChartPath map[string]helm.ChartDocumentationInfo) map[string]helm.ChartDocumentationInfo { + generateDirectories := []string{} + if len(generateDirectories) == 0 { + return documentationInfoByChartPath + } + documentationInfoToGenerate := make(map[string]helm.ChartDocumentationInfo, len(generateDirectories)) + var skipped = false + for _, chartDirectory := range generateDirectories { + if info, ok := documentationInfoByChartPath[chartDirectory]; ok { + documentationInfoToGenerate[chartDirectory] = info + } else { + log.Warnf("Couldn't find documentation Info for <%s> - skipping", chartDirectory) + skipped = true + } + } + if skipped { + possibleCharts := []string{} + for path := range documentationInfoByChartPath { + possibleCharts = append(possibleCharts, path) + } + log.Warnf("Some charts listed in `chart-to-generate` wasn't found. List of charts to choose: [%s]", strings.Join(possibleCharts, ", ")) + } + return documentationInfoToGenerate +} + +func writeDocumentation(chartSearchRoot string, documentationInfoByChartPath map[string]helm.ChartDocumentationInfo, dryRun bool, parallelism int) { + templateFiles := []string{"README.md.gotmpl"} + badgeStyle := "flat-square" + + log.Debugf("Rendering from optional template files [%s]", strings.Join(templateFiles, ", ")) + + documentDependencyValues := true + documentationInfoToGenerate := getChartToGenerate(documentationInfoByChartPath) + + parallelProcessIterable(documentationInfoToGenerate, parallelism, func(elem interface{}) { + info := documentationInfoByChartPath[elem.(string)] + var err error + var dependencyValues []document.DependencyValues + if documentDependencyValues { + dependencyValues, err = document.GetDependencyValues(info, documentationInfoByChartPath) + if err != nil { + log.Warnf("Error evaluating dependency values for chart %s, skipping: %v", info.ChartDirectory, err) + return + } + } + document.PrintDocumentation(info, chartSearchRoot, templateFiles, dryRun, "v1.11.0", badgeStyle, dependencyValues) + }) +} + +func Readme(chartSearchRoot string, dryRun bool) error { + parallelism := runtime.NumCPU() * 2 + log.SetLevel(log.FatalLevel) + viper.Set("values-file", "values.yaml") + viper.Set("output-file", "README.md") + + // On dry runs all output goes to stdout, and so as to not jumble things, generate serially. + if dryRun { + parallelism = 1 + } + + documentationInfoByChartPath, err := readDocumentationInfoByChartPath(chartSearchRoot, parallelism) + if err != nil { + return err + } + + writeDocumentation(chartSearchRoot, documentationInfoByChartPath, dryRun, parallelism) + return nil +} diff --git a/pkg/template/funcs.go b/pkg/template/funcs.go index 0a54f116..c2712db5 100644 --- a/pkg/template/funcs.go +++ b/pkg/template/funcs.go @@ -166,12 +166,14 @@ func chartInstalled(name, repoName string) (bool, error) { repo, err := client.GetRepository(repoName) if err != nil { - return false, err + fmt.Printf("error: %s\n", err) + return false, nil } chartInstallations, err := client.GetChartInstallations(repo.Id) if err != nil { - return false, err + fmt.Printf("error: %s\n", err) + return false, nil } for _, chartInstallation := range chartInstallations {