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

refactor: adds interface for blueprint loading #25

Merged
merged 1 commit into from
Sep 8, 2024
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
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