diff --git a/devfile.yaml b/devfile.yaml index 0508b153..db769a31 100644 --- a/devfile.yaml +++ b/devfile.yaml @@ -5,7 +5,9 @@ metadata: attributes: alpha.build-dockerfile: /relative/path/to/Dockerfile parent: - uri: https://raw.githubusercontent.com/odo-devfiles/registry/master/devfiles/nodejs/devfile.yaml + # uri: https://raw.githubusercontent.com/odo-devfiles/registry/master/devfiles/nodejs/devfile.yaml + id: nodejs + registryUrl: "https://registry.devfile.io" commands: - id: install exec: diff --git a/main.go b/main.go index 60cedcee..42d8817d 100644 --- a/main.go +++ b/main.go @@ -20,28 +20,27 @@ func main() { } } -//ParseDevfile to parse devfile from library -func ParseDevfile(devfileLocation string) (parser.DevfileObj, error) { - - devfile, err := devfilepkg.ParseAndValidate(devfileLocation) - return devfile, err -} - func parserTest() { - var devfile parser.DevfileObj - var err error + var args parser.ParserArgs if len(os.Args) > 1 { if strings.HasPrefix(os.Args[1], "http") { - devfile, err = devfilepkg.ParseFromURLAndValidate(os.Args[1]) + args = parser.ParserArgs{ + URL: os.Args[1], + } } else { - devfile, err = ParseDevfile(os.Args[1]) + args = parser.ParserArgs{ + Path: os.Args[1], + } } fmt.Println("parsing devfile from " + os.Args[1]) } else { - devfile, err = ParseDevfile("devfile.yaml") - fmt.Println("parsing devfile from " + devfile.Ctx.GetAbsPath()) + args = parser.ParserArgs{ + Path: "devfile.yaml", + } + fmt.Println("parsing devfile from ./devfile.yaml") } + devfile, err := devfilepkg.ParseDevfileAndValidate(args) if err != nil { fmt.Println(err) } else { diff --git a/pkg/devfile/parse.go b/pkg/devfile/parse.go index cebc9fab..92a858ed 100644 --- a/pkg/devfile/parse.go +++ b/pkg/devfile/parse.go @@ -9,6 +9,7 @@ import ( // and validates the devfile integrity with the schema // and validates the devfile data. // Creates devfile context and runtime objects. +// Deprecated, use ParseDevfileAndValidate() instead func ParseFromURLAndValidate(url string) (d parser.DevfileObj, err error) { // read and parse devfile from the given URL @@ -30,6 +31,7 @@ func ParseFromURLAndValidate(url string) (d parser.DevfileObj, err error) { // and validates the devfile integrity with the schema // and validates the devfile data. // Creates devfile context and runtime objects. +// Deprecated, use ParseDevfileAndValidate() instead func ParseFromDataAndValidate(data []byte) (d parser.DevfileObj, err error) { // read and parse devfile from the given bytes d, err = parser.ParseFromData(data) @@ -49,6 +51,7 @@ func ParseFromDataAndValidate(data []byte) (d parser.DevfileObj, err error) { // and validates the devfile integrity with the schema // and validates the devfile data. // Creates devfile context and runtime objects. +// Deprecated, use ParseDevfileAndValidate() instead func ParseAndValidate(path string) (d parser.DevfileObj, err error) { // read and parse devfile from given path @@ -65,3 +68,22 @@ func ParseAndValidate(path string) (d parser.DevfileObj, err error) { return d, err } + +// ParseDevfileAndValidate func parses the devfile data +// and validates the devfile integrity with the schema +// and validates the devfile data. +// Creates devfile context and runtime objects. +func ParseDevfileAndValidate(args parser.ParserArgs) (d parser.DevfileObj, err error) { + d, err = parser.ParseDevfile(args) + if err != nil { + return d, err + } + + // generic validation on devfile content + err = validate.ValidateDevfileData(d.Data) + if err != nil { + return d, err + } + + return d, err +} diff --git a/pkg/devfile/parser/context/context.go b/pkg/devfile/parser/context/context.go index fa51fa7e..6dd99de5 100644 --- a/pkg/devfile/parser/context/context.go +++ b/pkg/devfile/parser/context/context.go @@ -35,6 +35,9 @@ type DevfileCtx struct { // trace of all url referenced uriMap map[string]bool + + // registry URLs list + registryURLs []string } // NewDevfileCtx returns a new DevfileCtx type object @@ -149,3 +152,13 @@ func (d *DevfileCtx) GetURIMap() map[string]bool { func (d *DevfileCtx) SetURIMap(uriMap map[string]bool) { d.uriMap = uriMap } + +// GetRegistryURLs func returns current devfile registry URLs +func (d *DevfileCtx) GetRegistryURLs() []string { + return d.registryURLs +} + +// SetRegistryURLs set registry URLs in the devfile ctx +func (d *DevfileCtx) SetRegistryURLs(registryURLs []string) { + d.registryURLs = registryURLs +} diff --git a/pkg/devfile/parser/parse.go b/pkg/devfile/parser/parse.go index 9de58979..38398f68 100644 --- a/pkg/devfile/parser/parse.go +++ b/pkg/devfile/parser/parse.go @@ -3,6 +3,7 @@ package parser import ( "encoding/json" "fmt" + "github.com/devfile/library/pkg/util" "net/url" "path" "strings" @@ -53,60 +54,108 @@ func parseDevfile(d DevfileObj, flattenedDevfile bool) (DevfileObj, error) { return d, nil } -// Parse func populates the flattened devfile data, parses and validates the devfile integrity. +// ParserArgs is the struct to pass into parser functions which contains required info for parsing devfile. +// It accepts devfile path, devfile URL or devfile content in []byte format. +type ParserArgs struct { + // Path is a relative or absolute devfile path. + Path string + // URL is the URL address of the specific devfile. + URL string + // Data is the devfile content in []byte format. + Data []byte + // FlattenedDevfile defines if the returned devfileObj is flattened content (true) or raw content (false). + // The value is default to be true. + FlattenedDevfile *bool + // RegistryURLs is a list of registry hosts which parser should pull parent devfile from. + // If registryUrl is defined in devfile, this list will be ignored. + RegistryURLs []string +} + +// ParseDevfile func populates the devfile data, parses and validates the devfile integrity. // Creates devfile context and runtime objects -func Parse(path string) (d DevfileObj, err error) { +func ParseDevfile(args ParserArgs) (d DevfileObj, err error) { + if args.Data != nil { + d.Ctx = devfileCtx.DevfileCtx{} + err = d.Ctx.SetDevfileContentFromBytes(args.Data) + if err != nil { + return d, errors.Wrap(err, "failed to set devfile content from bytes") + } + } else if args.Path != "" { + d.Ctx = devfileCtx.NewDevfileCtx(args.Path) + } else if args.URL != "" { + d.Ctx = devfileCtx.NewURLDevfileCtx(args.URL) + } else { + return d, errors.Wrap(err, "the devfile source is not provided") + } - // NewDevfileCtx - d.Ctx = devfileCtx.NewDevfileCtx(path) + if args.RegistryURLs != nil { + d.Ctx.SetRegistryURLs(args.RegistryURLs) + } + + flattenedDevfile := true + if args.FlattenedDevfile != nil { + flattenedDevfile = *args.FlattenedDevfile + } + + return populateAndParseDevfile(d, flattenedDevfile) +} + +func populateAndParseDevfile(d DevfileObj, flattenedDevfile bool) (DevfileObj, error) { + var err error // Fill the fields of DevfileCtx struct - err = d.Ctx.Populate() + if d.Ctx.GetURL() != "" { + err = d.Ctx.PopulateFromURL() + } else if d.Ctx.GetDevfileContent() != nil { + err = d.Ctx.PopulateFromRaw() + } else { + err = d.Ctx.Populate() + } if err != nil { return d, err } - return parseDevfile(d, true) + + return parseDevfile(d, flattenedDevfile) +} + +// Parse func populates the flattened devfile data, parses and validates the devfile integrity. +// Creates devfile context and runtime objects +// Deprecated, use ParseDevfile() instead +func Parse(path string) (d DevfileObj, err error) { + + // NewDevfileCtx + d.Ctx = devfileCtx.NewDevfileCtx(path) + + return populateAndParseDevfile(d, true) } // ParseRawDevfile populates the raw devfile data without overriding and merging +// Deprecated, use ParseDevfile() instead func ParseRawDevfile(path string) (d DevfileObj, err error) { // NewDevfileCtx d.Ctx = devfileCtx.NewDevfileCtx(path) - // Fill the fields of DevfileCtx struct - err = d.Ctx.Populate() - if err != nil { - return d, err - } - return parseDevfile(d, false) + return populateAndParseDevfile(d, false) } // ParseFromURL func parses and validates the devfile integrity. // Creates devfile context and runtime objects +// Deprecated, use ParseDevfile() instead func ParseFromURL(url string) (d DevfileObj, err error) { d.Ctx = devfileCtx.NewURLDevfileCtx(url) - // Fill the fields of DevfileCtx struct - err = d.Ctx.PopulateFromURL() - if err != nil { - return d, err - } - return parseDevfile(d, true) + return populateAndParseDevfile(d, true) } // ParseFromData func parses and validates the devfile integrity. // Creates devfile context and runtime objects +// Deprecated, use ParseDevfile() instead func ParseFromData(data []byte) (d DevfileObj, err error) { d.Ctx = devfileCtx.DevfileCtx{} err = d.Ctx.SetDevfileContentFromBytes(data) if err != nil { return d, errors.Wrap(err, "failed to set devfile content from bytes") } - err = d.Ctx.PopulateFromRaw() - if err != nil { - return d, err - } - - return parseDevfile(d, true) + return populateAndParseDevfile(d, true) } func parseParentAndPlugin(d DevfileObj) (err error) { @@ -121,8 +170,13 @@ func parseParentAndPlugin(d DevfileObj) (err error) { if err != nil { return err } + } else if parent.Id != "" { + parentDevfileObj, err = parseFromRegistry(parent.Id, parent.RegistryUrl, d.Ctx) + if err != nil { + return err + } } else { - return fmt.Errorf("parent URI undefined, currently only URI is suppported") + return fmt.Errorf("parent URI or parent Id undefined, currently only URI and Id are suppported") } parentWorkspaceContent := parentDevfileObj.Data.GetDevfileWorkspace() @@ -138,6 +192,7 @@ func parseParentAndPlugin(d DevfileObj) (err error) { klog.V(4).Infof("adding data of devfile with URI: %v", parent.Uri) } } + flattenedPlugins := []*v1.DevWorkspaceTemplateSpecContent{} components, err := d.Data.GetComponents(common.DevfileOptions{}) if err != nil { @@ -166,6 +221,7 @@ func parseParentAndPlugin(d DevfileObj) (err error) { flattenedPlugins = append(flattenedPlugins, flattenedPlugin) } } + mergedContent, err := apiOverride.MergeDevWorkspaceTemplateSpec(d.Data.GetDevfileWorkspace(), flattenedParent, flattenedPlugins...) if err != nil { return err @@ -190,20 +246,11 @@ func parseFromURI(uri string, curDevfileCtx devfileCtx.DevfileCtx) (DevfileObj, // relative path on disk if !absoluteURL && curDevfileCtx.GetAbsPath() != "" { d.Ctx = devfileCtx.NewDevfileCtx(path.Join(path.Dir(curDevfileCtx.GetAbsPath()), uri)) - d.Ctx.SetURIMap(curDevfileCtx.GetURIMap()) - - // Fill the fields of DevfileCtx struct - err = d.Ctx.Populate() - if err != nil { - return DevfileObj{}, err - } - return parseDevfile(d, true) - } - - // absolute URL address - if absoluteURL { + } else if absoluteURL { + // absolute URL address d.Ctx = devfileCtx.NewURLDevfileCtx(uri) } else if curDevfileCtx.GetURL() != "" { + // relative path to a URL u, err := url.Parse(curDevfileCtx.GetURL()) if err != nil { return DevfileObj{}, err @@ -212,11 +259,36 @@ func parseFromURI(uri string, curDevfileCtx devfileCtx.DevfileCtx) (DevfileObj, d.Ctx = devfileCtx.NewURLDevfileCtx(u.String()) } d.Ctx.SetURIMap(curDevfileCtx.GetURIMap()) - // Fill the fields of DevfileCtx struct - err = d.Ctx.PopulateFromURL() - if err != nil { - return DevfileObj{}, err + return populateAndParseDevfile(d, true) +} + +func parseFromRegistry(parentId, registryURL string, curDevfileCtx devfileCtx.DevfileCtx) (DevfileObj, error) { + if registryURL != "" { + devfileContent, err := getDevfileFromRegistry(parentId, registryURL) + if err != nil { + return DevfileObj{}, err + } + return ParseDevfile(ParserArgs{Data: devfileContent, RegistryURLs: curDevfileCtx.GetRegistryURLs()}) + } else if curDevfileCtx.GetRegistryURLs() != nil { + for _, registry := range curDevfileCtx.GetRegistryURLs() { + devfileContent, err := getDevfileFromRegistry(parentId, registry) + if devfileContent != nil && err == nil { + return ParseDevfile(ParserArgs{Data: devfileContent, RegistryURLs: curDevfileCtx.GetRegistryURLs()}) + } + } + } else { + return DevfileObj{}, fmt.Errorf("failed to fetch from registry, registry URL is not provided") } - return parseDevfile(d, true) + return DevfileObj{}, fmt.Errorf("failed to get parent Id: %s from registry URLs provided", parentId) +} + +func getDevfileFromRegistry(parentId, registryURL string) ([]byte, error) { + if !strings.HasPrefix(registryURL, "http://") && !strings.HasPrefix(registryURL, "https://") { + registryURL = fmt.Sprintf("http://%s", registryURL) + } + param := util.HTTPRequestParams{ + URL: fmt.Sprintf("%s/devfiles/%s", registryURL, parentId), + } + return util.HTTPGetRequest(param, 0) } diff --git a/pkg/devfile/parser/parse_test.go b/pkg/devfile/parser/parse_test.go index 0c96d42e..9e72c7f5 100644 --- a/pkg/devfile/parser/parse_test.go +++ b/pkg/devfile/parser/parse_test.go @@ -22,7 +22,7 @@ import ( const schemaV200 = "2.0.0" -func Test_parseParentAndPlugin(t *testing.T) { +func Test_parseParentAndPluginFromURI(t *testing.T) { type args struct { devFileObj DevfileObj @@ -38,7 +38,7 @@ func Test_parseParentAndPlugin(t *testing.T) { testRecursiveReference bool }{ { - name: "case 1: it should override the requested parent's data and add the local devfile's data", + name: "it should override the requested parent's data and add the local devfile's data", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -253,7 +253,7 @@ func Test_parseParentAndPlugin(t *testing.T) { }, }, { - name: "case 2: handle a parent'data without any local override and add the local devfile's data", + name: "handle a parent'data without any local override and add the local devfile's data", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -436,7 +436,7 @@ func Test_parseParentAndPlugin(t *testing.T) { }, }, { - name: "case 3: it should error out when the override is invalid", + name: "it should error out when the override is invalid", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -502,7 +502,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 4: error out if the same parent command is defined again in the local devfile", + name: "error out if the same parent command is defined again in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -555,7 +555,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 5: error out if the same parent component is defined again in the local devfile", + name: "error out if the same parent component is defined again in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -612,7 +612,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 6: should not have error if the same event is defined again in the local devfile", + name: "should not have error if the same event is defined again in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -669,7 +669,7 @@ func Test_parseParentAndPlugin(t *testing.T) { }, }, { - name: "case 7: error out if the parent project is defined again in the local devfile", + name: "error out if the parent project is defined again in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -714,7 +714,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 8: it should merge the plugin's uri data and add the local devfile's data", + name: "it should merge the plugin's uri data and add the local devfile's data", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -897,7 +897,7 @@ func Test_parseParentAndPlugin(t *testing.T) { }, }, { - name: "case 9: it should override the plugin's data with local overrides and add the local devfile's data", + name: "it should override the plugin's data with local overrides and add the local devfile's data", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1107,7 +1107,7 @@ func Test_parseParentAndPlugin(t *testing.T) { }, }, { - name: "case 10: it should error out when the plugin devfile is invalid", + name: "it should error out when the plugin devfile is invalid", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1152,7 +1152,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 11: error out if the same plugin command is defined again in the local devfile", + name: "error out if the same plugin command is defined again in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1205,7 +1205,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 12: error out if the same plugin component is defined again in the local devfile", + name: "error out if the same plugin component is defined again in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1262,7 +1262,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 13: error out if the plugin project is defined again in the local devfile", + name: "error out if the plugin project is defined again in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1307,7 +1307,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 14: error out if the same project is defined in the both plugin devfile and parent", + name: "error out if the same project is defined in the both plugin devfile and parent", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1371,7 +1371,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 15: error out if the same command is defined in both plugin devfile and parent devfile", + name: "error out if the same command is defined in both plugin devfile and parent devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1447,7 +1447,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 16: error out if the same component is defined in both plugin devfile and parent devfile", + name: "error out if the same component is defined in both plugin devfile and parent devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1529,7 +1529,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 17: it should override the requested parent's data and plugin's data, and add the local devfile's data", + name: "it should override the requested parent's data and plugin's data, and add the local devfile's data", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1784,7 +1784,7 @@ func Test_parseParentAndPlugin(t *testing.T) { }, }, { - name: "case 18: error out if the plugin component is defined with a different component type in the local devfile", + name: "error out if the plugin component is defined with a different component type in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1841,7 +1841,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 19: it should override with no errors if the plugin component is defined with a different component type in the plugin override", + name: "it should override with no errors if the plugin component is defined with a different component type in the plugin override", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1914,7 +1914,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: false, }, { - name: "case 20: error out if the parent component is defined with a different component type in the local devfile", + name: "error out if the parent component is defined with a different component type in the local devfile", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -1971,7 +1971,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: true, }, { - name: "case 21: it should override with no errors if the parent component is defined with a different component type in the parent override", + name: "it should override with no errors if the parent component is defined with a different component type in the parent override", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -2049,7 +2049,7 @@ func Test_parseParentAndPlugin(t *testing.T) { wantErr: false, }, { - name: "case 22: error out if the URI is recursively referenced", + name: "error out if the URI is recursively referenced", args: args{ devFileObj: DevfileObj{ Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), @@ -2400,6 +2400,289 @@ func Test_parseParentAndPlugin_RecursivelyReference_withMultipleURI(t *testing.T }) } +func Test_parseParentFromRegistry(t *testing.T) { + const validRegistry = "127.0.0.1:8080" + const invalidRegistry = "invalid-registry.io" + parentDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaV200, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + var data []byte + var err error + if strings.Contains(r.URL.Path, "/devfiles/nodejs") { + data, err = yaml.Marshal(parentDevfile.Data) + } else { + w.WriteHeader(http.StatusNotFound) + return + } + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + _, err = w.Write(data) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + })) + // create a listener with the desired port. + l, err := net.Listen("tcp", validRegistry) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + testServer.Listener.Close() + testServer.Listener = l + + testServer.Start() + defer testServer.Close() + + mainDevfileContent := v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + RegistryUrl: validRegistry, + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "nodejs", + }, + }, + ParentOverrides: v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "parent-runtime", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + WorkspaceEvents: v1.WorkspaceEvents{ + PostStop: []string{"post-stop"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + } + wantDevfileContent := v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + Events: &v1.Events{ + WorkspaceEvents: v1.WorkspaceEvents{ + PostStart: []string{}, + PostStop: []string{"post-stop"}, + PreStop: []string{}, + PreStart: []string{}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/projects", + Name: "nodejs-starter-build", + }, + }, + }, + }, + } + + ctxWithRegistry := devfileCtx.NewDevfileCtx(OutputDevfileYamlPath) + ctxWithRegistry.SetRegistryURLs([]string{validRegistry}) + + tests := []struct { + name string + mainDevfile DevfileObj + registryURI string + wantDevFile DevfileObj + wantErr bool + testRecursiveReference bool + }{ + { + name: "it should override the requested parent's data from provided registryURL and add the local devfile's data", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: mainDevfileContent, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: wantDevfileContent, + }, + }, + }, + { + name: "it should override the requested parent's data from registryURLs set in context and add the local devfile's data", + mainDevfile: DevfileObj{ + Ctx: ctxWithRegistry, + Data: &v2.DevfileV2{ + Devfile: mainDevfileContent, + }, + }, + wantDevFile: DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: wantDevfileContent, + }, + }, + }, + { + name: "it should error out with invalid registry provided", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "nodejs", + }, + RegistryUrl: invalidRegistry, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "it should error out with non-exist registry id provided", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "not-exist", + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + err := parseParentAndPlugin(tt.mainDevfile) + + // Unexpected error + if (err != nil) != tt.wantErr { + t.Errorf("parseParentAndPlugin() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Expected error and got an err + if tt.wantErr && err != nil { + return + } + + if !reflect.DeepEqual(tt.mainDevfile.Data, tt.wantDevFile.Data) { + t.Errorf("wanted: %v, got: %v, difference at %v", tt.wantDevFile.Data, tt.mainDevfile.Data, pretty.Compare(tt.mainDevfile.Data, tt.wantDevFile.Data)) + } + + }) + } +} + func Test_parseFromURI(t *testing.T) { const uri1 = "127.0.0.1:8080" const httpPrefix = "http://" @@ -2557,36 +2840,36 @@ func Test_parseFromURI(t *testing.T) { wantErr bool }{ { - name: "case 1: should be able to parse from relative uri on local disk", + name: "should be able to parse from relative uri on local disk", curDevfileCtx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), wantDevFile: localDevfile, uri: localRelativeURI, }, { - name: "case 2: should be able to parse relative uri from URL", + name: "should be able to parse relative uri from URL", curDevfileCtx: parentDevfile.Ctx, wantDevFile: relativeParentDevfile, uri: localRelativeURI, }, { - name: "case 3: should fail if no path or url has been set for devfile ctx", + name: "should fail if no path or url has been set for devfile ctx", curDevfileCtx: devfileCtx.DevfileCtx{}, wantErr: true, }, { - name: "case 4: should fail if file not exist", + name: "should fail if file not exist", curDevfileCtx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), uri: notExistURI, wantErr: true, }, { - name: "case 5: should fail if url not exist", + name: "should fail if url not exist", curDevfileCtx: devfileCtx.NewURLDevfileCtx(httpPrefix + uri1), uri: notExistURI, wantErr: true, }, { - name: "case 6: should fail if with invalid URI format", + name: "should fail if with invalid URI format", curDevfileCtx: devfileCtx.NewURLDevfileCtx(OutputDevfileYamlPath), uri: invalidURL, wantErr: true, @@ -2614,3 +2897,138 @@ func Test_parseFromURI(t *testing.T) { }) } } + +func Test_parseFromRegistry(t *testing.T) { + const ( + registry = "127.0.0.1:8080" + httpPrefix = "http://" + notExistId = "notexist" + invalidRegistry = "http//invalid.com" + registryId = "nodejs" + ) + + ctxWithRegistry := devfileCtx.NewDevfileCtx(OutputDevfileYamlPath) + ctxWithRegistry.SetRegistryURLs([]string{registry}) + + parentDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaV200, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var data []byte + var err error + if strings.Contains(r.URL.Path, "/devfiles/"+registryId) { + data, err = yaml.Marshal(parentDevfile.Data) + } else { + w.WriteHeader(http.StatusNotFound) + return + } + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + _, err = w.Write(data) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + })) + // create a listener with the desired port. + l, err := net.Listen("tcp", registry) + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + testServer.Listener.Close() + testServer.Listener = l + + testServer.Start() + defer testServer.Close() + + tests := []struct { + name string + curDevfileCtx devfileCtx.DevfileCtx + registryUrl string + registryId string + wantDevFile DevfileObj + wantErr bool + }{ + { + name: "should be able to parse from provided registryUrl without prefix", + curDevfileCtx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + wantDevFile: parentDevfile, + registryUrl: registry, + registryId: registryId, + }, + { + name: "should be able to parse from provided registryUrl with prefix", + curDevfileCtx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + wantDevFile: parentDevfile, + registryUrl: httpPrefix + registry, + registryId: registryId, + }, + { + name: "should be able to parse from registry URL defined in ctx", + curDevfileCtx: ctxWithRegistry, + wantDevFile: parentDevfile, + registryId: registryId, + }, + { + name: "should fail if registryId does not exist", + curDevfileCtx: devfileCtx.NewURLDevfileCtx(OutputDevfileYamlPath), + registryUrl: registry, + registryId: notExistId, + wantErr: true, + }, + { + name: "should fail if registryUrl is not provided, and no registry URLs has been set in ctx", + curDevfileCtx: devfileCtx.NewURLDevfileCtx(OutputDevfileYamlPath), + registryId: registryId, + wantErr: true, + }, + { + name: "should fail if registryUrl is invalid", + curDevfileCtx: devfileCtx.NewURLDevfileCtx(OutputDevfileYamlPath), + registryUrl: invalidRegistry, + registryId: registryId, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseFromRegistry(tt.registryId, tt.registryUrl, tt.curDevfileCtx) + if tt.wantErr == (err == nil) { + t.Errorf("Test_parseFromRegistry() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !reflect.DeepEqual(got.Data, tt.wantDevFile.Data) { + t.Errorf("wanted: %v, got: %v, difference at %v", tt.wantDevFile, got, pretty.Compare(tt.wantDevFile, got)) + } + }) + } +}