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

Initial catalog implementation #2815

Merged
merged 24 commits into from
Dec 8, 2023
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
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"})
brikis98 marked this conversation as resolved.
Show resolved Hide resolved

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