diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 5797963ed..47e601c29 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -21,6 +21,9 @@ COPY build-tools /build-tools COPY index/ /index COPY tests/registry /registry +# Download offline starter projects +RUN bash /build-tools/dl_starter_projects.sh go-starter community + # Run the registry build tools RUN /build-tools/build.sh /registry /build diff --git a/build-tools/build.sh b/build-tools/build.sh index 69098f8bf..1ac720f87 100755 --- a/build-tools/build.sh +++ b/build-tools/build.sh @@ -30,7 +30,8 @@ tar_files_and_cleanup() { -a -not -name "*.vsx" \ -a -not -name "." \ -a -not -name "logo.svg" \ - -a -not -name "logo.png" \) -maxdepth 1) + -a -not -name "logo.png" \ + -a -not -name "*.zip"\) -maxdepth 1) # There are files that need to be pulled into a tar archive if [[ ! -z $tarFiles ]]; then diff --git a/build-tools/dl_starter_projects.sh b/build-tools/dl_starter_projects.sh new file mode 100644 index 000000000..a21301cca --- /dev/null +++ b/build-tools/dl_starter_projects.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +if [[ -z "$@" ]] +then + echo "No starter projects specified." + exit 0 +fi + +# Path of stacks directory in the registry +STACKS_DIR=/registry/stacks +# List of starter projects to use offline +offline_starter_projects=( "$@" ) + +# Downloads a starter project from a remote git repository and packages it as a zip archive +# to be used as an offline resource. +download_git_starter_project() { + stack_root=$1 + name=$2 + remote_name=$(yq e ".starterProjects[] | select(.name == \"${name}\").git.checkoutFrom.remote" $stack_root/devfile.yaml) + revision=$(yq e ".starterProjects[] | select(.name == \"${name}\").git.checkoutFrom.revision" $stack_root/devfile.yaml) + subDir=$(yq e ".starterProjects[] | select(.name == \"${name}\").subDir" $stack_root/devfile.yaml) + local_path=${stack_root}/${name}-offline + + if [ "${remote_name}" == "null" ] + then + remote_url=$(yq e ".starterProjects[] | select(.name == \"${name}\").git.remotes.origin" $stack_root/devfile.yaml) + else + remote_url=$(yq e ".starterProjects[] | select(.name == \"${name}\").git.remotes.${remote_name}" $stack_root/devfile.yaml) + fi + + mkdir -p $local_path + + git clone $remote_url $local_path + + if [ "${revision}" != "null" ] + then + cd $local_path && git checkout $revision && cd - + fi + + if [ "${subDir}" != "null" ] + then + cd $local_path/$subDir && zip -q ${local_path}.zip * .[^.]* && cd - + else + cd $local_path && rm -rf ./.git && zip -q ${local_path}.zip * .[^.]* && cd - + fi + + rm -rf $local_path +} + +# Downloads a starter project from a remote zip archive source +# to be used as an offline resource. +download_zip_starter_project() { + stack_root=$1 + name=$2 + remote_url=$(yq e ".starterProjects[] | select(.name == \"${name}\").zip.location" $stack_root/devfile.yaml) + local_path=${stack_root}/${name}-offline + + curl -L $remote_url -o ${local_path}.zip +} + +# Read stacks list +read -r -a stacks <<< "$(ls ${STACKS_DIR} | tr '\n' ' ')" + +echo "Downloading offline starter projects.." +for starter_project in ${offline_starter_projects[@]} +do + for stack in ${stacks[@]} + do + stack_root=$STACKS_DIR/$stack + stack_devfile=$stack_root/devfile.yaml + # Read version list for stack + read -r -a versions <<< "$(ls ${STACKS_DIR}/${stack} | grep -e '[0-9].[0-9].[0-9]' | tr '\n' ' ')" + # If multi version stack + if [[ ${#versions[@]} -gt 0 ]] + then + for version in ${versions[@]} + do + stack_root=$STACKS_DIR/$stack/$version + stack_devfile=$stack_root/devfile.yaml + # If the specified starter project is found + if [ ! -z "$(yq e ".starterProjects[] | select(.name == \"${starter_project}\")" $stack_devfile)" ] + then + # Starter project has a git remote + if [ "$(yq e ".starterProjects[] | select(.name == \"${starter_project}\").git" $stack_devfile)" != "null" ] + then + echo "Downloading ${starter_project} starter project in stack ${stack} version ${version}.." + download_git_starter_project $stack_root $starter_project + echo "Downloading ${starter_project} starter project in stack ${stack} version ${version}..done!" + # Starter project has a zip remote + elif [ "$(yq e ".starterProjects[] | select(.name == \"${starter_project}\").zip" $stack_devfile)" != "null" ] + then + echo "Downloading ${starter_project} starter project in stack ${stack} version ${version}.." + download_zip_starter_project $stack_root $starter_project + echo "Downloading ${starter_project} starter project in stack ${stack} version ${version}..done!" + fi + fi + done + # If not multi version stack & the specified starter project is found + elif [ ! -z "$(yq e ".starterProjects[] | select(.name == \"${starter_project}\")" $stack_devfile)" ] + then + # Starter project has a git remote + if [ "$(yq e ".starterProjects[] | select(.name == \"${starter_project}\").git" $stack_devfile)" != "null" ] + then + echo "Downloading ${starter_project} starter project in stack ${stack}.." + download_git_starter_project $stack_root $starter_project + echo "Downloading ${starter_project} starter project in stack ${stack}..done!" + # Starter project has a zip remote + elif [ "$(yq e ".starterProjects[] | select(.name == \"${starter_project}\").zip" $stack_devfile)" != "null" ] + then + echo "Downloading ${starter_project} starter project in stack ${stack}.." + download_zip_starter_project $stack_root $starter_project + echo "Downloading ${starter_project} starter project in stack ${stack}..done!" + fi + fi + done +done +echo "Downloading offline starter projects..done!" diff --git a/index/server/pkg/server/endpoint.go b/index/server/pkg/server/endpoint.go index 3017e5d98..2467f294c 100644 --- a/index/server/pkg/server/endpoint.go +++ b/index/server/pkg/server/endpoint.go @@ -15,12 +15,11 @@ import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + dfutil "github.com/devfile/library/pkg/util" libutil "github.com/devfile/registry-support/index/generator/library" - "github.com/devfile/registry-support/index/generator/schema" indexSchema "github.com/devfile/registry-support/index/generator/schema" "github.com/devfile/registry-support/index/server/pkg/util" "github.com/gin-gonic/gin" - versionpkg "github.com/hashicorp/go-version" "github.com/prometheus/client_golang/prometheus" "gopkg.in/segmentio/analytics-go.v3" ) @@ -131,7 +130,22 @@ func serveDevfileStarterProjectWithVersion(c *gin.Context) { version := c.Param("version") starterProjectName := c.Param("starterProjectName") downloadTmpLoc := path.Join("/tmp", starterProjectName) - devfileBytes, _ := fetchDevfile(c, devfileName, version) // TODO: add devfileIndex when telemetry is migrated + stackLoc := path.Join(stacksPath, devfileName) + devfileBytes, devfileIndex := fetchDevfile(c, devfileName, version) + + if len(devfileIndex.Versions) > 1 { + versionMap, err := util.MakeVersionMap(devfileIndex) + if err != nil { + log.Print(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + "status": "failed to parse the stack version", + }) + return + } + + stackLoc = path.Join(stackLoc, versionMap[version].Version) + } if len(devfileBytes) == 0 { // fetchDevfile was unsuccessful (error or not found) @@ -169,7 +183,7 @@ func serveDevfileStarterProjectWithVersion(c *gin.Context) { } if starterProject := starterProjects[0]; starterProject.Git != nil { - gitScheme := schema.Git{ + gitScheme := indexSchema.Git{ Remotes: starterProject.Git.Remotes, RemoteName: "origin", SubDir: starterProject.SubDir, @@ -194,14 +208,78 @@ func serveDevfileStarterProjectWithVersion(c *gin.Context) { return } } else if starterProject.Zip != nil { - downloadBytes, err = libutil.DownloadStackFromZipUrl(starterProject.Zip.Location, starterProject.SubDir, downloadTmpLoc) - if err != nil { - log.Print(err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - "status": fmt.Sprintf("Problem with downloading starter project %s", starterProjectName), - }) - return + if _, err = url.ParseRequestURI(starterProject.Zip.Location); err != nil { + localLoc := path.Join(stackLoc, starterProject.Zip.Location) + log.Printf("zip location is not a valid http url: %v\nTrying local path %s..", err, localLoc) + + // If subdirectory is specified for starter project download then extract subdirectory + // and create new archive for download. + if starterProject.SubDir != "" { + downloadFilePath := fmt.Sprintf("%s.zip", downloadTmpLoc) + + if _, err = os.Stat(downloadTmpLoc); os.IsExist(err) { + err = os.Remove(downloadTmpLoc) + if err != nil { + log.Print(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + "status": fmt.Sprintf("Problem removing existing temporary download directory '%s' for starter project %s", + downloadTmpLoc, + starterProjectName), + }) + return + } + } + + _, err = dfutil.Unzip(localLoc, downloadTmpLoc, starterProject.SubDir) + if err != nil { + log.Print(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + "status": fmt.Sprintf("Problem with reading subDir '%s' of starter project %s at %s", + starterProject.SubDir, + starterProjectName, + localLoc), + }) + return + } + + err = libutil.ZipDir(downloadTmpLoc, downloadFilePath) + if err != nil { + log.Print(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + "status": fmt.Sprintf("Problem with archiving subDir '%s' of starter project %s at %s", + starterProject.SubDir, + starterProjectName, + downloadFilePath), + }) + return + } + + localLoc = downloadFilePath + } + + downloadBytes, err = ioutil.ReadFile(localLoc) + if err != nil { + log.Print(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + "status": fmt.Sprintf("Problem with reading starter project %s at %s", starterProjectName, + localLoc), + }) + return + } + } else { + downloadBytes, err = libutil.DownloadStackFromZipUrl(starterProject.Zip.Location, starterProject.SubDir, downloadTmpLoc) + if err != nil { + log.Print(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + "status": fmt.Sprintf("Problem with downloading starter project %s", starterProjectName), + }) + return + } } } else { c.JSON(http.StatusBadRequest, gin.H{ @@ -430,44 +508,25 @@ func fetchDevfile(c *gin.Context, name string, version string) ([]byte, indexSch sampleDevfilePath = path.Join(samplesPath, devfileIndex.Name, devfileName) } } else { - versionMap := make(map[string]indexSchema.Version) - var latestVersion string - for _, versionElement := range devfileIndex.Versions { - versionMap[versionElement.Version] = versionElement - if versionElement.Default { - versionMap["default"] = versionElement - } - if latestVersion != "" { - latest, err := versionpkg.NewVersion(latestVersion) - if err != nil { - log.Print(err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - "status": fmt.Sprintf("failed to parse the stack version %s for stack %s", latestVersion, name), - }) - return []byte{}, indexSchema.Schema{} - } - current, err := versionpkg.NewVersion(versionElement.Version) + versionMap, err := util.MakeVersionMap(devfileIndex) + if err != nil { + log.Print(err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + "status": "failed to parse the stack version", + }) + return []byte{}, indexSchema.Schema{} + } + if foundVersion, ok := versionMap[version]; ok { + if devfileIndex.Type == indexSchema.StackDevfileType { + bytes, err = pullStackFromRegistry(foundVersion) if err != nil { - log.Print(err.Error()) c.JSON(http.StatusInternalServerError, gin.H{ "error": err.Error(), - "status": fmt.Sprintf("failed to parse the stack version %s for stack %s", versionElement.Version, name), + "status": fmt.Sprintf("Problem pulling version %s from OCI Registry", foundVersion.Version), }) return []byte{}, indexSchema.Schema{} } - if current.GreaterThan(latest) { - latestVersion = versionElement.Version - } - } else { - latestVersion = versionElement.Version - } - } - versionMap["latest"] = versionMap[latestVersion] - - if foundVersion, ok := versionMap[version]; ok { - if devfileIndex.Type == indexSchema.StackDevfileType { - bytes, err = pullStackFromRegistry(foundVersion) } else { // Retrieve the sample devfile stored under /registry/samples/ sampleDevfilePath = path.Join(samplesPath, devfileIndex.Name, foundVersion.Version, devfileName) diff --git a/index/server/pkg/server/registry.go b/index/server/pkg/server/registry.go index ffbfe7dbb..718440f8f 100644 --- a/index/server/pkg/server/registry.go +++ b/index/server/pkg/server/registry.go @@ -9,6 +9,7 @@ import ( "os" "path" "path/filepath" + "strings" indexSchema "github.com/devfile/registry-support/index/generator/schema" @@ -24,8 +25,8 @@ func pushStackToRegistry(versionComponent indexSchema.Version, stackName string) memoryStore := content.NewMemoryStore() pushContents := []ocispec.Descriptor{} for _, resource := range versionComponent.Resources { - if resource == "meta.yaml" { - // Some registries may still have the meta.yaml in it, but we don't need it, so skip pushing it up + if resource == "meta.yaml" || strings.HasSuffix(resource, "-offline.zip") { + // Some registries may still have the meta.yaml (we don't need it) or offline resources in it, so skip pushing these up continue } diff --git a/index/server/pkg/util/util.go b/index/server/pkg/util/util.go index 618b43da1..5048d12ed 100644 --- a/index/server/pkg/util/util.go +++ b/index/server/pkg/util/util.go @@ -11,6 +11,8 @@ import ( "strconv" "strings" + versionpkg "github.com/hashicorp/go-version" + indexLibrary "github.com/devfile/registry-support/index/generator/library" indexSchema "github.com/devfile/registry-support/index/generator/schema" ) @@ -182,3 +184,32 @@ func IsTelemetryEnabled() bool { } return false } + +// MakeVersionMap creates a map of versions for a given devfile index schema. +func MakeVersionMap(devfileIndex indexSchema.Schema) (map[string]indexSchema.Version, error) { + versionMap := make(map[string]indexSchema.Version) + var latestVersion string + for _, versionElement := range devfileIndex.Versions { + versionMap[versionElement.Version] = versionElement + if versionElement.Default { + versionMap["default"] = versionElement + } + if latestVersion != "" { + latest, err := versionpkg.NewVersion(latestVersion) + if err != nil { + return map[string]indexSchema.Version{}, err + } + current, err := versionpkg.NewVersion(versionElement.Version) + if err != nil { + return map[string]indexSchema.Version{}, err + } + if current.GreaterThan(latest) { + latestVersion = versionElement.Version + } + } else { + latestVersion = versionElement.Version + } + } + versionMap["latest"] = versionMap[latestVersion] + return versionMap, nil +} diff --git a/index/server/pkg/util/util_test.go b/index/server/pkg/util/util_test.go index 158ba07fe..14c8f927b 100644 --- a/index/server/pkg/util/util_test.go +++ b/index/server/pkg/util/util_test.go @@ -2,11 +2,13 @@ package util import ( "encoding/json" - "github.com/devfile/registry-support/index/generator/schema" + "fmt" "io/ioutil" "os" "reflect" "testing" + + indexSchema "github.com/devfile/registry-support/index/generator/schema" ) func TestIsHtmlRequested(t *testing.T) { @@ -206,12 +208,12 @@ func TestConvertToOldIndexFormat(t *testing.T) { if err != nil { t.Errorf("Failed to oldIndexStruct.json: %v", err) } - var inputIndex []schema.Schema + var inputIndex []indexSchema.Schema err = json.Unmarshal(bytes, &inputIndex) if err != nil { t.Errorf("Failed to unmarshal inputIndex json") } - var wantIndex []schema.Schema + var wantIndex []indexSchema.Schema err = json.Unmarshal(expected, &wantIndex) if err != nil { t.Errorf("Failed to unmarshal wantIndex json") @@ -225,3 +227,105 @@ func TestConvertToOldIndexFormat(t *testing.T) { } }) } + +func TestMakeVersionMap(t *testing.T) { + devfileIndex := indexSchema.Schema{ + Name: "Test Devfile", + Version: "2.2.0", + Attributes: nil, + DisplayName: "", + Description: "", + Type: "", + Tags: []string{}, + Architectures: []string{}, + Icon: "", + GlobalMemoryLimit: "", + ProjectType: "", + Language: "", + Links: map[string]string{}, + Resources: []string{}, + StarterProjects: []string{}, + Git: &indexSchema.Git{}, + Provider: "", + SupportUrl: "", + Versions: []indexSchema.Version{ + { + Version: "1.1.0", + Default: true, + }, + {Version: "1.2.0"}, + }, + } + tests := []struct { + key string + wantVal string + }{ + { + key: "default", + wantVal: "1.1.0", + }, + { + key: "latest", + wantVal: "1.2.0", + }, + { + key: "1.1.0", + wantVal: "1.1.0", + }, + { + key: "", + wantVal: "", + }, + { + key: "1.3.0", + wantVal: "", + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("Test generate version map with key %s", test.key), func(t *testing.T) { + versionMap, err := MakeVersionMap(devfileIndex) + if err != nil { + t.Errorf("Was not expecting error with MakeVersionMap: %v", err) + } + + if !reflect.DeepEqual(test.wantVal, versionMap[test.key].Version) { + t.Errorf("Was expecting '%s' to map to '%s' not '%s'", + test.key, test.wantVal, versionMap[test.key].Version) + } + }) + } +} + +func TestMakeVersionMapOnBadVersion(t *testing.T) { + devfileIndex := indexSchema.Schema{ + Name: "Test Devfile", + Version: "2.2.0", + Attributes: nil, + DisplayName: "", + Description: "", + Type: "", + Tags: []string{}, + Architectures: []string{}, + Icon: "", + GlobalMemoryLimit: "", + ProjectType: "", + Language: "", + Links: map[string]string{}, + Resources: []string{}, + StarterProjects: []string{}, + Git: &indexSchema.Git{}, + Provider: "", + SupportUrl: "", + Versions: []indexSchema.Version{ + {Version: "fsdf-sf3v.-dfg"}, + {Version: "erdgf-v.-dd-,.fdgg"}, + }, + } + t.Run("Test generate version map with bad versioning", func(t *testing.T) { + _, err := MakeVersionMap(devfileIndex) + if err == nil { + t.Error("Was expecting malformed version error with MakeVersionMap") + } + }) +} diff --git a/tests/integration/pkg/tests/indexserver_tests.go b/tests/integration/pkg/tests/indexserver_tests.go index bcf82add0..4b4c2e582 100644 --- a/tests/integration/pkg/tests/indexserver_tests.go +++ b/tests/integration/pkg/tests/indexserver_tests.go @@ -318,6 +318,64 @@ var _ = ginkgo.Describe("[Verify index server is working properly]", func() { })) }) + ginkgo.It("/devfiles//starter-projects/ endpoint should return an offline zip archive for devfile starter project", func() { + resp, err := http.Get(config.Registry + "/devfiles/go/starter-projects/go-starter-offline") + var bytes []byte + + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + defer resp.Body.Close() + + bytes, err = ioutil.ReadAll(resp.Body) + + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(resp.StatusCode).To(gomega.Equal(http.StatusAccepted)) + gomega.Expect(bytes).ToNot(gomega.BeEmpty()) + gomega.Expect(bytes).To(gomega.Satisfy(func(file []byte) bool { + return http.DetectContentType(file) == "application/zip" + })) + }) + + ginkgo.It("/devfiles///starter-projects/ endpoint should return an offline zip archive for devfile starter project", func() { + resp, err := http.Get(config.Registry + "/devfiles/go/1.2.0/starter-projects/go-starter-offline") + var bytes []byte + + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + defer resp.Body.Close() + + bytes, err = ioutil.ReadAll(resp.Body) + + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(resp.StatusCode).To(gomega.Equal(http.StatusAccepted)) + gomega.Expect(bytes).ToNot(gomega.BeEmpty()) + gomega.Expect(bytes).To(gomega.Satisfy(func(file []byte) bool { + return http.DetectContentType(file) == "application/zip" + })) + }) + + ginkgo.It("/devfiles//starter-projects/ endpoint should return an offline zip archive of a subdir for devfile starter project", func() { + resp, err := http.Get(config.Registry + "/devfiles/java-quarkus/starter-projects/community-offline") + var bytes []byte + + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + defer resp.Body.Close() + + bytes, err = ioutil.ReadAll(resp.Body) + + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(resp.StatusCode).To(gomega.Equal(http.StatusAccepted)) + gomega.Expect(bytes).ToNot(gomega.BeEmpty()) + gomega.Expect(bytes).To(gomega.Satisfy(func(file []byte) bool { + return http.DetectContentType(file) == "application/zip" + })) + }) + + ginkgo.It("/devfiles//starter-projects/ endpoint should return an error for an offline starter project file location that doesn't exist", func() { + resp, err := http.Get(config.Registry + "/devfiles/java-maven/starter-projects/springbootproject-offline") + + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + gomega.Expect(resp.StatusCode).To(gomega.Equal(http.StatusInternalServerError)) + }) + ginkgo.It("/devfiles//starter-projects/ endpoint should return an error for a devfile that doesn't exist", func() { resp, err := http.Get(config.Registry + "/devfiles/fake-stack/starter-projects/springbootproject") diff --git a/tests/registry/stacks/go/1.1.0/devfile.yaml b/tests/registry/stacks/go/1.1.0/devfile.yaml index 001d47077..d4726d07f 100644 --- a/tests/registry/stacks/go/1.1.0/devfile.yaml +++ b/tests/registry/stacks/go/1.1.0/devfile.yaml @@ -17,6 +17,9 @@ starterProjects: revision: main remotes: origin: https://github.com/devfile-samples/devfile-stack-go.git + - name: go-starter-offline + zip: + location: go-starter-offline.zip components: - container: endpoints: diff --git a/tests/registry/stacks/go/1.2.0/devfile.yaml b/tests/registry/stacks/go/1.2.0/devfile.yaml index a879fa8fd..4cb263fd6 100644 --- a/tests/registry/stacks/go/1.2.0/devfile.yaml +++ b/tests/registry/stacks/go/1.2.0/devfile.yaml @@ -17,6 +17,9 @@ starterProjects: revision: main remotes: origin: https://github.com/devfile-samples/devfile-stack-go.git + - name: go-starter-offline + zip: + location: go-starter-offline.zip components: - container: endpoints: diff --git a/tests/registry/stacks/java-maven/devfile.yaml b/tests/registry/stacks/java-maven/devfile.yaml index 76ae4a6af..ee80996bf 100644 --- a/tests/registry/stacks/java-maven/devfile.yaml +++ b/tests/registry/stacks/java-maven/devfile.yaml @@ -12,6 +12,9 @@ metadata: - "arm64" - "s390x" starterProjects: + - name: springbootproject-offline + zip: + location: springbootproject.zip - name: springbootproject git: remotes: diff --git a/tests/registry/stacks/java-quarkus/devfile.yaml b/tests/registry/stacks/java-quarkus/devfile.yaml index 62c29c557..786562b6e 100644 --- a/tests/registry/stacks/java-quarkus/devfile.yaml +++ b/tests/registry/stacks/java-quarkus/devfile.yaml @@ -11,6 +11,10 @@ metadata: architectures: - "amd64" starterProjects: + - name: community-offline + zip: + location: community-offline.zip + subDir: src/main/java/org/acme - name: community zip: location: https://code.quarkus.io/d?e=io.quarkus%3Aquarkus-resteasy&e=io.quarkus%3Aquarkus-micrometer&e=io.quarkus%3Aquarkus-smallrye-health&e=io.quarkus%3Aquarkus-openshift&cn=devfile