Skip to content

Commit

Permalink
Initial catalog implementation (#2815)
Browse files Browse the repository at this point in the history
* feat: disallow undefined flags

* chore: first implementation

* chore: find modules

* chore: get repo and find modules

* chore: comment code

* chore: enhance repo link handling

* chore: minor changes

* chore: unit test

* fix: unit test

* fix: unit test

* chore: .gitignore

* fix: unit test

* chore: rename testdata path

* chore: clean code

* chore: code comment

* fix: critical PR issues

* chore: remove unused var

* chore: integrate scaffold in the catalog

* chore: update comments

* fix: scaffold path

* fix: unit test

* fix: increase timeout for golangci-lint
  • Loading branch information
levkohimins authored Dec 8, 2023
1 parent 5aca88d commit 3514cad
Show file tree
Hide file tree
Showing 38 changed files with 2,062 additions and 27 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ install-lint:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2

run-lint:
golangci-lint run -v ./...
golangci-lint run -v --timeout=5m ./...

.PHONY: help fmtcheck fmt install-fmt-hook clean install-lint run-lint
2 changes: 2 additions & 0 deletions cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/gruntwork-io/go-commons/env"
"github.com/gruntwork-io/terragrunt/cli/commands"
awsproviderpatch "github.com/gruntwork-io/terragrunt/cli/commands/aws-provider-patch"
"github.com/gruntwork-io/terragrunt/cli/commands/catalog"
graphdependencies "github.com/gruntwork-io/terragrunt/cli/commands/graph-dependencies"
"github.com/gruntwork-io/terragrunt/cli/commands/hclfmt"
outputmodulegroups "github.com/gruntwork-io/terragrunt/cli/commands/output-module-groups"
Expand Down Expand Up @@ -77,6 +78,7 @@ func terragruntCommands(opts *options.TerragruntOptions) cli.Commands {
renderjson.NewCommand(opts), // render-json
awsproviderpatch.NewCommand(opts), // aws-provider-patch
outputmodulegroups.NewCommand(opts), // output-module-groups
catalog.NewCommand(opts), // catalog
scaffold.NewCommand(opts), // scaffold
}

Expand Down
3 changes: 2 additions & 1 deletion cli/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ func TestAutocomplete(t *testing.T) {
}{
{
"",
[]string{"aws-provider-patch", "graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "scaffold", "terragrunt-info", "validate-inputs"},
[]string{"aws-provider-patch", "graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "terragrunt-info", "validate-inputs"},
},
{
"--versio",
Expand All @@ -497,6 +497,7 @@ func TestAutocomplete(t *testing.T) {

output := &bytes.Buffer{}
app := NewApp(output, os.Stderr)
app.Commands = app.Commands.Filter([]string{"aws-provider-patch", "graph-dependencies", "hclfmt", "output-module-groups", "render-json", "run-all", "terragrunt-info", "validate-inputs"})

err := app.Run([]string{"terragrunt"})
require.NoError(t, err)
Expand Down
32 changes: 32 additions & 0 deletions cli/commands/catalog/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package catalog

import (
"context"

"github.com/gruntwork-io/terragrunt/cli/commands/catalog/module"
"github.com/gruntwork-io/terragrunt/cli/commands/catalog/tui"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/pkg/log"
"github.com/pkg/errors"
)

func Run(ctx context.Context, opts *options.TerragruntOptions, repoPath string) error {
log.SetLogger(opts.Logger.Logger)

repo, err := module.NewRepo(ctx, repoPath)
if err != nil {
return err
}
//nolint:errcheck
defer repo.RemoveTempData()

modules, err := repo.FindModules(ctx)
if err != nil {
return err
}
if len(modules) == 0 {
return errors.Errorf("specified repository %q does not contain modules", repoPath)
}

return tui.Run(ctx, modules, opts)
}
27 changes: 27 additions & 0 deletions cli/commands/catalog/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package catalog

import (
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/pkg/cli"
)

const (
CommandName = "catalog"
)

func NewCommand(opts *options.TerragruntOptions) *cli.Command {
return &cli.Command{
Name: CommandName,
DisallowUndefinedFlags: true,
Usage: "Launch the user interface for searching and managing your module catalog.",
Action: func(ctx *cli.Context) error {
var repoPath string

if val := ctx.Args().Get(0); val != "" {
repoPath = val
}

return Run(ctx, opts.OptionsFromContext(ctx), repoPath)
},
}
}
205 changes: 205 additions & 0 deletions cli/commands/catalog/module/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package module

import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/gruntwork-io/go-commons/collections"
"github.com/gruntwork-io/go-commons/errors"
"github.com/gruntwork-io/terragrunt/pkg/log"
)

const (
mdHeader = "#"
adocHeader = "="
)

var (
// `strings.EqualFold` is used (case insensitive) while comparing
acceptableReadmeFiles = []string{"README.md", "README.adoc"}

mdHeaderReg = regexp.MustCompile(`(?m)^#{1}\s?([^#][\S\s]+)`)
adocHeaderReg = regexp.MustCompile(`(?m)^={1}\s?([^=][\S\s]+)`)

commentReg = regexp.MustCompile(`<!--[\S\s]*?-->`)
adocImageReg = regexp.MustCompile(`image:[^\]]+]`)

terraformFileExts = []string{".tf"}
ignoreFiles = []string{"terraform-cloud-enterprise-private-module-registry-placeholder.tf"}

defaultDescription = "(no description found)"
)

type Modules []*Module

type Module struct {
repoPath string
moduleDir string
url string
title string
description string
readme string
}

// NewModule returns a module instance if the given `moduleDir` path contains a Terraform module, otherwise returns nil.
func NewModule(repo *Repo, moduleDir string) (*Module, error) {
module := &Module{
repoPath: repo.path,
moduleDir: moduleDir,
title: filepath.Base(moduleDir),
description: defaultDescription,
}

if ok, err := module.isValid(); !ok || err != nil {
return nil, err
}

log.Debugf("Found module in directory %q", moduleDir)

moduleURL, err := repo.moduleURL(moduleDir)
if err != nil {
return nil, err
}
module.url = moduleURL

if err := module.parseReadme(); err != nil {
return nil, err
}

return module, nil
}

// Title implements /github.com/charmbracelet/bubbles.list.DefaultItem.Title
func (module *Module) Title() string {
return module.title
}

// Description implements /github.com/charmbracelet/bubbles.list.DefaultItem.Description
func (module *Module) Description() string {
return module.description
}

func (module *Module) Readme() string {
return module.readme
}

// FilterValue implements /github.com/charmbracelet/bubbles.list.Item.FilterValue
func (module *Module) FilterValue() string {
return module.title
}

func (module *Module) URL() string {
return module.url
}

func (module *Module) Path() string {
return fmt.Sprintf("%s//%s", module.repoPath, module.moduleDir)

}

func (module *Module) isValid() (bool, error) {
files, err := os.ReadDir(filepath.Join(module.repoPath, module.moduleDir))
if err != nil {
return false, errors.WithStackTrace(err)
}

for _, file := range files {
if file.IsDir() {
continue
}

if collections.ListContainsElement(ignoreFiles, file.Name()) {
continue
}

ext := filepath.Ext(file.Name())
if collections.ListContainsElement(terraformFileExts, ext) {
return true, nil
}
}

return false, nil
}

func (module *Module) parseReadme() error {
var readmePath string

modulePath := filepath.Join(module.repoPath, module.moduleDir)

files, err := os.ReadDir(modulePath)
if err != nil {
return errors.WithStackTrace(err)
}

for _, file := range files {
if file.IsDir() {
continue
}

for _, readmeFile := range acceptableReadmeFiles {
if strings.EqualFold(readmeFile, file.Name()) {
readmePath = filepath.Join(modulePath, file.Name())
break
}
}

// `md` files have priority over `adoc` files
if strings.EqualFold(filepath.Ext(readmePath), ".md") {
break
}
}

if readmePath == "" {
return nil
}

readmeByte, err := os.ReadFile(readmePath)
if err != nil {
return errors.WithStackTrace(err)
}
module.readme = string(readmeByte)

var (
reg = mdHeaderReg
docHeader = mdHeader
)

if strings.HasSuffix(readmePath, ".adoc") {
reg = adocHeaderReg
docHeader = adocHeader
}

if match := reg.FindStringSubmatch(module.readme); len(match) > 0 {
header := match[1]

// remove comments
header = commentReg.ReplaceAllString(header, "")
// remove adoc images
header = adocImageReg.ReplaceAllString(header, "")

lines := strings.Split(header, "\n")
module.title = strings.TrimSpace(lines[0])

var descriptionLines []string

if len(lines) > 1 {
for _, line := range lines[1:] {
line = strings.TrimSpace(line)

// another header begins
if strings.HasPrefix(line, docHeader) {
break
}

descriptionLines = append(descriptionLines, line)
}
}

module.description = strings.TrimSpace(strings.Join(descriptionLines, " "))
}

return nil
}
Loading

0 comments on commit 3514cad

Please sign in to comment.