Skip to content

Commit

Permalink
add coverage command
Browse files Browse the repository at this point in the history
Signed-off-by: James Petersen <[email protected]>
  • Loading branch information
found-it committed Apr 12, 2024
1 parent acd922b commit c60010a
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 3 deletions.
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module github.com/chainguard-images/images

go 1.21
go 1.21.2

toolchain go1.21.6
toolchain go1.22.1

require (
chainguard.dev/apko v0.13.3
github.com/chainguard-dev/clog v1.3.1
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/hcl/v2 v2.20.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmms
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/chainguard-dev/clog v1.3.1 h1:CDNCty5WKQhJzoOPubk0GdXt+bPQyargmfClqebrpaQ=
github.com/chainguard-dev/clog v1.3.1/go.mod h1:cV516KZWqYc/phZsCNwF36u/KMGS+Gj5Uqeb8Hlp95Y=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
Expand Down
2 changes: 1 addition & 1 deletion monopod/pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func New() *cobra.Command {
"Root of commands.")

// Add sub-commands.
cmd.AddCommand(Matrix(), Readme(), Lint(), Scaffold(), version.Version())
cmd.AddCommand(Matrix(), Readme(), Lint(), Scaffold(), Coverage(), version.Version())

return cmd
}
252 changes: 252 additions & 0 deletions monopod/pkg/commands/coverage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
package commands

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/chainguard-dev/clog"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/spf13/cobra"

"github.com/chainguard-images/images/monopod/pkg/images"
)

func Coverage() *cobra.Command {
cmd := &cobra.Command{
Use: "coverage",
Short: "Generate a test coverage report",
Example: `
monopod coverage
`,
RunE: func(cmd *cobra.Command, args []string) error {
impl := &coverageImpl{}
return impl.Do(cmd.Context())
},
}

return cmd
}

// nabbed from https://github.com/hashicorp/terraform/blob/d1761f436b636da959ad5865591d53edbe9df8b3/internal/configs/parser_config.go#L273
var configFileSchema = &hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: "terraform",
},
{
// This one is not really valid, but we include it here so we
// can create a specialized error message hinting the user to
// nest it inside a "terraform" block.
Type: "required_providers",
},
{
Type: "provider",
LabelNames: []string{"name"},
},
{
Type: "variable",
LabelNames: []string{"name"},
},
{
Type: "locals",
},
{
Type: "output",
LabelNames: []string{"name"},
},
{
Type: "module",
LabelNames: []string{"name"},
},
{
Type: "resource",
LabelNames: []string{"type", "name"},
},
{
Type: "data",
LabelNames: []string{"type", "name"},
},
{
Type: "moved",
},
{
Type: "removed",
},
{
Type: "import",
},
{
Type: "check",
LabelNames: []string{"name"},
},
},
}

type coverageImpl struct {
modules []*moduleContext
}

// moduleContext is the parsing context for an individual terraform module.
type moduleContext struct {
Module string `json:"module"`
p *hclparse.Parser `json:"-"`
HasTest bool `json:"has_test"`
IsImagetest bool `json:"is_imagetest"`
}

func (i *coverageImpl) Do(ctx context.Context) error {
return i.generateCoverageReport(ctx)
}

func (i *coverageImpl) generateCoverageReport(ctx context.Context) error {
// log := clog.FromContext(ctx)
allImages, err := images.ListAll()
if err != nil {
return err
}

sort.Slice(allImages, func(i, j int) bool {
return allImages[i].ImageName < allImages[j].ImageName
})

for _, img := range allImages {
// log.Infof("./images/%s/main.tf\n", i.ImageName)
mainTf := fmt.Sprintf("./images/%s/main.tf", img.ImageName)

c := moduleContext{
Module: img.ImageName,
p: hclparse.NewParser(),
}

if err := c.hunt(ctx, mainTf); err != nil {
return fmt.Errorf("hunting terraform files: %w", err)
}

if err := c.huntTest(ctx); err != nil {
return fmt.Errorf("hunting terraform files: %w", err)
}

i.modules = append(i.modules, &c)
}

f, _ := os.Create("output.json")
defer f.Close()
b, err := json.Marshal(i.modules)
if err != nil {
return err
}
f.Write(b)
return nil
}

// hunt hunts for terraform files used by the top level module. It simply
// parses the files and looks for more. The hclparse.Parser saves a map of
// all files it has parsed so we are just preprocessing and collecting all
// files here.
//
// It ignores tf files in tflib right now. There is no strict standard on module
// names so we instead we ignore modules coming from a common source (rather
// than trying to regex all `publisher/latest/etc` module names)
func (c *moduleContext) hunt(ctx context.Context, file string) error {
// log := clog.FromContext(ctx)
f, diags := c.p.ParseHCLFile(file)
if diags != nil {
return fmt.Errorf("%v", diags)
}

content, diags := f.Body.Content(configFileSchema)
if diags != nil {
return fmt.Errorf("%v", diags)
}

for _, b := range content.Blocks {
if b.Type != "module" {
continue
}

att, diags := b.Body.JustAttributes()
if diags != nil {
return fmt.Errorf("%v", diags)
}

// Get the source attribute of the block
if val, ok := att["source"]; ok {
// Pass a nil context, assuming there are no expressions in
// the source string. This will fail if a variable expression
// exists in the string
//
// TODO: handle context
// TODO: don't kill the whole process if one module errs
v, diags := val.Expr.Value(nil)
if diags != nil {
return fmt.Errorf("%v", diags)
}

// Ignore files in tflib
if strings.Contains(v.AsString(), "tflib") {
continue
}

// Get path of current parsed file
path := filepath.Join(filepath.Dir(file), v.AsString())
files, err := filepath.Glob(filepath.Join(path, "*.tf"))
if err != nil {
return err
}

for _, tfFile := range files {
c.hunt(ctx, tfFile)
}
}
}
return nil
}

func (c *moduleContext) huntTest(ctx context.Context) error {
log := clog.FromContext(ctx)

c.HasTest = false

for filename, tfFile := range c.p.Files() {
log.Infof("|-- %s", filename)
content, diags := tfFile.Body.Content(configFileSchema)
if diags != nil {
return fmt.Errorf("%v", diags)
}

for _, b := range content.Blocks {
log.Infof("|---- %s", b.Type)
for _, l := range b.Labels {
log.Infof("|------ %s", l)
for _, key := range []string{
"oci_exec_test",
"imagetest_inventory",
"imagetest_harness_",
"imagetest_feature",
"helm_release",
"helm-cleanup",
} {
if strings.Contains(l, key) {
c.HasTest = true
return nil
}
}
}
}
}
return nil
}

func (c *moduleContext) String() string {
b, err := json.Marshal(c)
if err != nil {
panic(err)
}
return string(b)
}

0 comments on commit c60010a

Please sign in to comment.