diff --git a/pkg/devfile/handler.go b/pkg/devfile/handler.go index e4c394fd40a..544181e15d3 100644 --- a/pkg/devfile/handler.go +++ b/pkg/devfile/handler.go @@ -34,6 +34,35 @@ func DevfileSamplesHandler(w http.ResponseWriter, r *http.Request) { w.Write(sampleIndex) } +// parseDevfileWithFallback attempts to parse the devfile with full parent/plugin +// resolution (flattened). If that fails (e.g. the parent registry is unreachable or +// the OCI pull times out), it retries without flattening. The unflattened parse +// still provides all locally-defined components and commands, which is sufficient +// for the outer-loop resource generation the console backend performs. +func parseDevfileWithFallback(devfileContentBytes []byte, httpTimeout *int) (parser.DevfileObj, error) { + devfileObj, _, err := devfile.ParseDevfileAndValidate(parser.ParserArgs{ + Data: devfileContentBytes, + HTTPTimeout: httpTimeout, + }) + if err == nil { + return devfileObj, nil + } + + klog.Warningf("Flattened devfile parse failed, retrying without parent resolution: %v", err) + + flattenedDevfile := false + devfileObj, _, err = devfile.ParseDevfileAndValidate(parser.ParserArgs{ + Data: devfileContentBytes, + HTTPTimeout: httpTimeout, + FlattenedDevfile: &flattenedDevfile, + }) + if err != nil { + return parser.DevfileObj{}, err + } + + return devfileObj, nil +} + func DevfileHandler(w http.ResponseWriter, r *http.Request) { var ( data DevfileForm @@ -48,11 +77,10 @@ func DevfileHandler(w http.ResponseWriter, r *http.Request) { return } - // Get devfile content and parse it using a library call in the future devfileContentBytes := []byte(data.Devfile.DevfileContent) - //reduce the http request and response timeouts on the devfile library parser to 10s httpTimeout := 10 - devfileObj, _, err = devfile.ParseDevfileAndValidate(parser.ParserArgs{Data: devfileContentBytes, HTTPTimeout: &httpTimeout}) + + devfileObj, err = parseDevfileWithFallback(devfileContentBytes, &httpTimeout) if err != nil { errMsg := "Failed to parse devfile:" if strings.Contains(err.Error(), "schemaVersion not present in devfile") { diff --git a/pkg/devfile/handler_test.go b/pkg/devfile/handler_test.go new file mode 100644 index 00000000000..ca20ca2eea3 --- /dev/null +++ b/pkg/devfile/handler_test.go @@ -0,0 +1,137 @@ +package devfile + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const validDevfileNoParent = ` +schemaVersion: 2.2.0 +metadata: + name: test-app + attributes: + alpha.dockerimage-port: 8080 +components: + - name: image-build + image: + imageName: test-image:latest + dockerfile: + uri: Dockerfile + buildContext: . + - name: kubernetes-deploy + kubernetes: + inlined: | + kind: Deployment + apiVersion: apps/v1 + metadata: + name: my-deploy + spec: + replicas: 1 + selector: + matchLabels: + app: test-app + template: + metadata: + labels: + app: test-app + spec: + containers: + - name: my-container + image: test-image:latest +commands: + - id: build-image + apply: + component: image-build + - id: deployk8s + apply: + component: kubernetes-deploy + - id: deploy + composite: + commands: + - build-image + - deployk8s + group: + kind: deploy + isDefault: true +` + +const devfileWithBadRegistry = ` +schemaVersion: 2.2.0 +metadata: + name: test +parent: + id: nonexistent-stack + registryUrl: 'https://does-not-exist.invalid' +components: + - name: image-build + image: + imageName: test:latest + dockerfile: + uri: Dockerfile + buildContext: . + - name: kubernetes-deploy + kubernetes: + inlined: | + kind: Deployment + apiVersion: apps/v1 + metadata: + name: test-deploy + spec: + replicas: 1 + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + containers: + - name: test + image: test:latest +commands: + - id: build-image + apply: + component: image-build + - id: deployk8s + apply: + component: kubernetes-deploy + - id: deploy + composite: + commands: + - build-image + - deployk8s + group: + kind: deploy + isDefault: true +` + +func TestParseDevfileWithFallback(t *testing.T) { + httpTimeout := 10 + + t.Run("devfile without parent parses on first attempt", func(t *testing.T) { + devfileObj, err := parseDevfileWithFallback([]byte(validDevfileNoParent), &httpTimeout) + assert.NoError(t, err) + + components, err := GetDeployComponents(devfileObj) + assert.NoError(t, err) + assert.Contains(t, components, "image-build") + assert.Contains(t, components, "kubernetes-deploy") + }) + + t.Run("devfile with unreachable parent falls back to unflattened parse", func(t *testing.T) { + devfileObj, err := parseDevfileWithFallback([]byte(devfileWithBadRegistry), &httpTimeout) + assert.NoError(t, err, "fallback to unflattened parse should succeed") + + components, err := GetDeployComponents(devfileObj) + assert.NoError(t, err) + assert.Contains(t, components, "image-build") + assert.Contains(t, components, "kubernetes-deploy") + }) + + t.Run("completely invalid devfile fails both attempts", func(t *testing.T) { + _, err := parseDevfileWithFallback([]byte("not valid yaml: ["), &httpTimeout) + assert.Error(t, err) + }) +}