Skip to content

Commit

Permalink
refactor: adds interface for blueprint loading (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmgilman authored Sep 8, 2024
1 parent 28d94dd commit 4ab6cee
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 88 deletions.
18 changes: 13 additions & 5 deletions blueprint/pkg/blueprint/raw.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
package blueprint

import "cuelang.org/go/cue"
import (
"cuelang.org/go/cue"
"github.com/input-output-hk/catalyst-forge/blueprint/schema"
)

// RawBlueprint represents a raw (undecoded) blueprint.
type RawBlueprint struct {
value cue.Value
}

// Decode decodes the raw blueprint into the given value.
func (r RawBlueprint) Decode(x interface{}) error {
return r.value.Decode(x)
// Decode decodes the raw blueprint into a schema.Blueprint.
func (r *RawBlueprint) Decode() (schema.Blueprint, error) {
var cfg schema.Blueprint
if err := r.value.Decode(&cfg); err != nil {
return schema.Blueprint{}, err
}

return cfg, nil
}

// DecodePath decodes a value from the raw blueprint.
// DecodePath decodes a path from the raw blueprint to the given interface.
func (r RawBlueprint) DecodePath(path string, x interface{}) error {
v := r.Get(path)
return v.Decode(x)
Expand Down
81 changes: 29 additions & 52 deletions blueprint/pkg/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,27 @@ var (
ErrVersionNotFound = errors.New("version not found")
)

type BlueprintLoader struct {
blueprint blueprint.RawBlueprint
injector injector.Injector
logger *slog.Logger
rootPath string
walker walker.ReverseWalker
// BlueprintLoader is an interface for loading blueprints.
type BlueprintLoader interface {

// Load loads the blueprint.
Load() blueprint.RawBlueprint
}

// DefaultBlueprintLoader is the default implementation of the BlueprintLoader
type DefaultBlueprintLoader struct {
injector injector.Injector
logger *slog.Logger
rootPath string
walker walker.ReverseWalker
}

func (b *BlueprintLoader) Load() error {
func (b *DefaultBlueprintLoader) Load() (blueprint.RawBlueprint, error) {
b.logger.Info("Searching for git root", "rootPath", b.rootPath)
gitRoot, err := b.findGitRoot(b.rootPath)
if err != nil && !errors.Is(err, ErrGitRootNotFound) {
b.logger.Error("Failed to find git root", "error", err)
return fmt.Errorf("failed to find git root: %w", err)
return blueprint.RawBlueprint{}, fmt.Errorf("failed to find git root: %w", err)
}

var files map[string][]byte
Expand All @@ -47,22 +54,22 @@ func (b *BlueprintLoader) Load() error {
files, err = b.findBlueprints(b.rootPath, b.rootPath)
if err != nil {
b.logger.Error("Failed to find blueprint files", "error", err)
return fmt.Errorf("failed to find blueprint files: %w", err)
return blueprint.RawBlueprint{}, fmt.Errorf("failed to find blueprint files: %w", err)
}
} else {
b.logger.Info("Git root found, searching for blueprint files up to git root", "gitRoot", gitRoot)
files, err = b.findBlueprints(b.rootPath, gitRoot)
if err != nil {
b.logger.Error("Failed to find blueprint files", "error", err)
return fmt.Errorf("failed to find blueprint files: %w", err)
return blueprint.RawBlueprint{}, fmt.Errorf("failed to find blueprint files: %w", err)
}
}

ctx := cuecontext.New()
schema, err := schema.LoadSchema(ctx)
if err != nil {
b.logger.Error("Failed to load schema", "error", err)
return fmt.Errorf("failed to load schema: %w", err)
return blueprint.RawBlueprint{}, fmt.Errorf("failed to load schema: %w", err)
}

var finalBlueprint cue.Value
Expand All @@ -74,21 +81,21 @@ func (b *BlueprintLoader) Load() error {
bp, err := blueprint.NewBlueprintFile(ctx, path, data, b.injector)
if err != nil {
b.logger.Error("Failed to load blueprint file", "path", path, "error", err)
return fmt.Errorf("failed to load blueprint file: %w", err)
return blueprint.RawBlueprint{}, fmt.Errorf("failed to load blueprint file: %w", err)
}

bps = append(bps, bp)
}

if err := bps.ValidateMajorVersions(); err != nil {
b.logger.Error("Major version mismatch")
return err
return blueprint.RawBlueprint{}, err
}

userBlueprint, err := bps.Unify(ctx)
if err != nil {
b.logger.Error("Failed to unify blueprint files", "error", err)
return fmt.Errorf("failed to unify blueprint files: %w", err)
return blueprint.RawBlueprint{}, fmt.Errorf("failed to unify blueprint files: %w", err)
}

finalVersion = bps.Version()
Expand All @@ -102,40 +109,25 @@ func (b *BlueprintLoader) Load() error {

if err := cuetools.Validate(finalBlueprint, cue.Concrete(true)); err != nil {
b.logger.Error("Failed to validate full blueprint", "error", err)
return err
return blueprint.RawBlueprint{}, err
}

if err := version.ValidateVersions(finalVersion, schema.Version); err != nil {
if errors.Is(err, version.ErrMinorMismatch) {
b.logger.Warn("The minor version of the blueprint is greater than the supported version", "version", finalVersion)
} else {
b.logger.Error("The major version of the blueprint is greater than the supported version", "version", finalVersion)
return fmt.Errorf("the major version of the blueprint (%s) is different than the supported version: cannot continue", finalVersion.String())
return blueprint.RawBlueprint{}, fmt.Errorf("the major version of the blueprint (%s) is different than the supported version: cannot continue", finalVersion.String())
}
}

b.blueprint = blueprint.NewRawBlueprint(finalBlueprint)
return nil
}

func (b *BlueprintLoader) Decode() (schema.Blueprint, error) {
var cfg schema.Blueprint
if err := b.blueprint.Decode(&cfg); err != nil {
return schema.Blueprint{}, err
}

return cfg, nil
}

// Raw returns the raw blueprint CUE value.
func (b *BlueprintLoader) Raw() blueprint.RawBlueprint {
return b.blueprint
return blueprint.NewRawBlueprint(finalBlueprint), nil
}

// findBlueprints searches for blueprint files starting from the startPath and
// ending at the endPath. It returns a map of blueprint file paths to their
// contents or an error if the search fails.
func (b *BlueprintLoader) findBlueprints(startPath, endPath string) (map[string][]byte, error) {
func (b *DefaultBlueprintLoader) findBlueprints(startPath, endPath string) (map[string][]byte, error) {
bps := make(map[string][]byte)

err := b.walker.Walk(
Expand Down Expand Up @@ -174,7 +166,7 @@ func (b *BlueprintLoader) findBlueprints(startPath, endPath string) (map[string]
// findGitRoot finds the root of a Git repository starting from the given
// path. It returns the path to the root of the Git repository or an error if
// the root is not found.
func (b *BlueprintLoader) findGitRoot(startPath string) (string, error) {
func (b *DefaultBlueprintLoader) findGitRoot(startPath string) (string, error) {
var gitRoot string
err := b.walker.Walk(
startPath,
Expand Down Expand Up @@ -202,34 +194,19 @@ func (b *BlueprintLoader) findGitRoot(startPath string) (string, error) {
return gitRoot, nil
}

// NewDefaultBlueprintLoader creates a new blueprint loader with default
// settings and an optional logger.
// NewDefaultBlueprintLoader creates a new DefaultBlueprintLoader.
func NewDefaultBlueprintLoader(rootPath string,
logger *slog.Logger,
) BlueprintLoader {
) DefaultBlueprintLoader {
if logger == nil {
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
}

walker := walker.NewDefaultFSReverseWalker(logger)
return BlueprintLoader{
return DefaultBlueprintLoader{
injector: injector.NewDefaultInjector(logger),
logger: logger,
rootPath: rootPath,
walker: &walker,
}
}

// NewBlueprintLoader creates a new blueprint loader
func NewBlueprintLoader(rootPath string,
logger *slog.Logger,
walker walker.ReverseWalker,
injector injector.Injector,
) BlueprintLoader {
return BlueprintLoader{
injector: injector,
logger: logger,
rootPath: rootPath,
walker: walker,
}
}
11 changes: 5 additions & 6 deletions blueprint/pkg/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"strings"
"testing"

"cuelang.org/go/cue"
"github.com/input-output-hk/catalyst-forge/blueprint/pkg/injector"
imocks "github.com/input-output-hk/catalyst-forge/blueprint/pkg/injector/mocks"
"github.com/input-output-hk/catalyst-forge/tools/pkg/testutils"
Expand Down Expand Up @@ -230,7 +229,7 @@ func TestBlueprintLoaderLoad(t *testing.T) {
},
}

loader := BlueprintLoader{
loader := DefaultBlueprintLoader{
injector: injector.NewInjector(
slog.New(slog.NewTextHandler(io.Discard, nil)),
&imocks.EnvGetterMock{
Expand All @@ -244,13 +243,13 @@ func TestBlueprintLoaderLoad(t *testing.T) {
walker: walker,
}

err := loader.Load()
bp, err := loader.Load()
if testutils.AssertError(t, err, tt.expectErr, "") {
return
}

for _, test := range tt.want {
value := loader.blueprint.Value().LookupPath(cue.ParsePath(test.fieldPath))
value := bp.Get(test.fieldPath)
assert.Nil(t, value.Err(), "failed to lookup field %s: %v", test.fieldPath, value.Err())

switch test.fieldType {
Expand Down Expand Up @@ -330,7 +329,7 @@ func TestBlueprintLoader_findBlueprints(t *testing.T) {
},
}

loader := BlueprintLoader{
loader := DefaultBlueprintLoader{
walker: walker,
}
got, err := loader.findBlueprints("/tmp", "/tmp")
Expand Down Expand Up @@ -400,7 +399,7 @@ func TestBlueprintLoader_findGitRoot(t *testing.T) {
},
}

loader := BlueprintLoader{
loader := DefaultBlueprintLoader{
walker: walker,
}
got, err := loader.findGitRoot(tt.start)
Expand Down
18 changes: 8 additions & 10 deletions forge/cli/cmd/cmds/blueprint_dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,24 @@ import (
"fmt"
"log/slog"
"os"

"github.com/input-output-hk/catalyst-forge/blueprint/pkg/loader"
)

type DumpCmd struct {
Config string `arg:"" help:"Path to the blueprint file."`
Pretty bool `help:"Pretty print JSON output."`
Blueprint string `arg:"" help:"Path to the blueprint file."`
Pretty bool `help:"Pretty print JSON output."`
}

func (c *DumpCmd) Run(logger *slog.Logger) error {
if _, err := os.Stat(c.Config); os.IsNotExist(err) {
return fmt.Errorf("blueprint file does not exist: %s", c.Config)
if _, err := os.Stat(c.Blueprint); os.IsNotExist(err) {
return fmt.Errorf("blueprint file does not exist: %s", c.Blueprint)
}

loader := loader.NewDefaultBlueprintLoader(c.Config, logger)
if err := loader.Load(); err != nil {
return err
rbp, err := loadRawBlueprint(c.Blueprint, logger)
if err != nil {
return fmt.Errorf("could not load blueprint: %w", err)
}

json, err := loader.Raw().MarshalJSON()
json, err := rbp.MarshalJSON()
if err != nil {
return err
}
Expand Down
13 changes: 4 additions & 9 deletions forge/cli/cmd/cmds/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"log/slog"
"strings"

"github.com/input-output-hk/catalyst-forge/blueprint/pkg/loader"
"github.com/input-output-hk/catalyst-forge/blueprint/schema"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/secrets"
)
Expand Down Expand Up @@ -41,13 +40,11 @@ func (c *Get) Run(logger *slog.Logger) error {
var maps map[string]string

if c.Blueprint != "" {
loader := loader.NewDefaultBlueprintLoader(c.Blueprint, logger)
if err := loader.Load(); err != nil {
rbp, err := loadRawBlueprint(c.Blueprint, logger)
if err != nil {
return fmt.Errorf("could not load blueprint: %w", err)
}

rbp := loader.Raw()

var secret schema.Secret
if err := rbp.DecodePath(c.Path, &secret); err != nil {
return fmt.Errorf("could not decode secret: %w", err)
Expand Down Expand Up @@ -132,13 +129,11 @@ func (c *Set) Run(logger *slog.Logger) error {
var path, provider string

if c.Blueprint != "" {
loader := loader.NewDefaultBlueprintLoader(c.Blueprint, logger)
if err := loader.Load(); err != nil {
rbp, err := loadRawBlueprint(c.Blueprint, logger)
if err != nil {
return fmt.Errorf("could not load blueprint: %w", err)
}

rbp := loader.Raw()

var secret schema.Secret
if err := rbp.DecodePath(c.Path, &secret); err != nil {
return fmt.Errorf("could not decode secret: %w", err)
Expand Down
17 changes: 11 additions & 6 deletions forge/cli/cmd/cmds/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
"fmt"
"log/slog"

blueprint "github.com/input-output-hk/catalyst-forge/blueprint/pkg/loader"
"github.com/input-output-hk/catalyst-forge/blueprint/pkg/blueprint"
"github.com/input-output-hk/catalyst-forge/blueprint/pkg/loader"
"github.com/input-output-hk/catalyst-forge/blueprint/schema"
"github.com/input-output-hk/catalyst-forge/forge/cli/pkg/earthly"
)
Expand Down Expand Up @@ -86,19 +87,23 @@ func generateOpts(target string, flags *RunCmd, config *schema.Blueprint) []eart

// loadBlueprint loads the blueprint file from the given root path.
func loadBlueprint(rootPath string, logger *slog.Logger) (schema.Blueprint, error) {
loader := blueprint.NewDefaultBlueprintLoader(rootPath, logger)

err := loader.Load()
raw, err := loadRawBlueprint(rootPath, logger)
if err != nil {
return schema.Blueprint{}, fmt.Errorf("failed loading blueprint: %w", err)
}

config, err := loader.Decode()
bp, err := raw.Decode()
if err != nil {
return schema.Blueprint{}, fmt.Errorf("failed decoding blueprint: %w", err)
}

return config, nil
return bp, nil
}

// loadRawBlueprint loads the raw blueprint file from the given root path.
func loadRawBlueprint(rootPath string, logger *slog.Logger) (blueprint.RawBlueprint, error) {
loader := loader.NewDefaultBlueprintLoader(rootPath, logger)
return loader.Load()
}

// printJson prints the given data as a JSON string.
Expand Down

0 comments on commit 4ab6cee

Please sign in to comment.