diff --git a/internal/blueprintload/blueprintload.go b/internal/blueprintload/blueprintload.go new file mode 100644 index 0000000..e06135c --- /dev/null +++ b/internal/blueprintload/blueprintload.go @@ -0,0 +1,67 @@ +package blueprintload + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/BurntSushi/toml" + + "github.com/osbuild/images/pkg/blueprint" +) + +// XXX: move this helper into images, share with bib +func decodeToml(r io.Reader, what string) (*blueprint.Blueprint, error) { + dec := toml.NewDecoder(r) + + var conf blueprint.Blueprint + _, err := dec.Decode(&conf) + if err != nil { + return nil, fmt.Errorf("cannot decode %q: %w", what, err) + } + + return &conf, nil +} + +func decodeJson(r io.Reader, what string) (*blueprint.Blueprint, error) { + dec := json.NewDecoder(r) + dec.DisallowUnknownFields() + + var conf blueprint.Blueprint + if err := dec.Decode(&conf); err != nil { + return nil, fmt.Errorf("cannot decode %q: %w", what, err) + } + if dec.More() { + return nil, fmt.Errorf("multiple configuration objects or extra data found in %q", what) + } + return &conf, nil +} + +func Load(path string) (*blueprint.Blueprint, error) { + var fp io.ReadCloser + var err error + + switch path { + case "": + return &blueprint.Blueprint{}, nil + case "-": + fp = os.Stdin + default: + fp, err = os.Open(path) + if err != nil { + return nil, err + } + defer fp.Close() + } + + switch { + case path == "-", filepath.Ext(path) == ".json": + return decodeJson(fp, path) + case filepath.Ext(path) == ".toml": + return decodeToml(fp, path) + default: + return nil, fmt.Errorf("unsupported file extension for %q (please use .toml or .json)", path) + } +} diff --git a/internal/blueprintload/blueprintload_test.go b/internal/blueprintload/blueprintload_test.go new file mode 100644 index 0000000..de7ad84 --- /dev/null +++ b/internal/blueprintload/blueprintload_test.go @@ -0,0 +1,72 @@ +package blueprintload_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/osbuild/images/pkg/blueprint" + + "github.com/osbuild/image-builder-cli/internal/blueprintload" +) + +var testBlueprintJSON = `{ + "customizations": { + "user": [ + { + "name": "alice" + } + ] + } +}` + +var testBlueprintTOML = ` +[[customizations.user]] +name = "alice" +` + +var expectedBlueprint = &blueprint.Blueprint{ + Customizations: &blueprint.Customizations{ + User: []blueprint.UserCustomization{ + { + Name: "alice", + }, + }, + }, +} + +func makeTestBlueprint(t *testing.T, name, content string) string { + tmpdir := t.TempDir() + blueprintPath := filepath.Join(tmpdir, name) + err := os.WriteFile(blueprintPath, []byte(content), 0644) + assert.NoError(t, err) + return blueprintPath +} + +func TestBlueprintLoadJSON(t *testing.T) { + for _, tc := range []struct { + fname string + content string + + expectedBp *blueprint.Blueprint + expectedError string + }{ + {"bp.json", testBlueprintJSON, expectedBlueprint, ""}, + {"bp.toml", testBlueprintTOML, expectedBlueprint, ""}, + {"bp.toml", "wrong-content", nil, `cannot decode .*/bp.toml": toml: `}, + {"bp.json", "wrong-content", nil, `cannot decode .*/bp.json": invalid `}, + {"bp", "wrong-content", nil, `unsupported file extension for "/.*/bp"`}, + } { + blueprintPath := makeTestBlueprint(t, tc.fname, tc.content) + bp, err := blueprintload.Load(blueprintPath) + if tc.expectedError == "" { + assert.NoError(t, err) + assert.Equal(t, tc.expectedBp, bp) + } else { + assert.NotNil(t, err) + assert.Regexp(t, tc.expectedError, err.Error()) + } + } +}