diff --git a/.dockerignore b/.dockerignore index 05edb62..0c360a5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,7 @@ packages/*/src packages/*/node_modules plugins *.local.yaml +.github +compose.yml +*.md +demo-catalog diff --git a/.github/workflows/commit-stage.yml b/.github/workflows/commit-stage.yml index b704f2b..f1d67df 100644 --- a/.github/workflows/commit-stage.yml +++ b/.github/workflows/commit-stage.yml @@ -10,7 +10,7 @@ env: jobs: build: name: Build - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: contents: read steps: @@ -20,25 +20,41 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version: 20 cache: 'yarn' - name: Install dependencies run: | yarn install --frozen-lockfile + + - name: Validate configuration + run: | + yarn backstage-cli config:check --lax + + - name: Validate types and definitions + run: | + yarn tsc:full - name: Prettier run: | - yarn run prettier:check + yarn prettier:check - - name: Compile and test + - name: Build + run: | + yarn build:all + + - name: Lint + run: | + yarn lint:all + + - name: Test run: | - yarn run ci + yarn test:all package: if: ${{ github.ref == 'refs/heads/main' }} needs: [ build ] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: contents: read packages: write @@ -55,7 +71,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v4 with: - node-version-file: '.nvmrc' + node-version: 20 cache: 'yarn' - name: Install dependencies @@ -101,7 +117,7 @@ jobs: registry: ${{ env.REGISTRY }} - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 id: build with: context: . @@ -127,7 +143,7 @@ jobs: merge: if: ${{ github.ref == 'refs/heads/main' }} needs: [package] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: contents: read packages: write @@ -144,7 +160,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Install Cosign - uses: sigstore/cosign-installer@v3.4.0 + uses: sigstore/cosign-installer@v3.6.0 - name: Generate Docker meta information id: meta diff --git a/.gitignore b/.gitignore index 7f87397..fbf8139 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ node_modules/ !.yarn/sdks !.yarn/versions +# Node version directives +.nvmrc + # dotenv environment variables file .env .env.test diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index 2edeafb..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -20 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index b9b01f6..bf9c615 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,6 +3,7 @@ dist-types coverage .vscode .github -docker-compose.yml +compose.yml CODE_OF_CONDUCT.md -MAINTAINERS.md \ No newline at end of file +MAINTAINERS.md +demo-catalog \ No newline at end of file diff --git a/app-config.production.yaml b/app-config.production.yaml index 94f10a8..d7c20b9 100644 --- a/app-config.production.yaml +++ b/app-config.production.yaml @@ -1,25 +1,35 @@ app: + # Should be the same as backend.baseUrl when using the `app-backend` plugin. baseUrl: ${BACKSTAGE_BASE_URL} backend: - auth: - keys: - - secret: ${BACKSTAGE_BACKEND_SECRET} + # Note that the baseUrl should be the URL that the browser and other clients + # should use when communicating with the backend, i.e. it needs to be + # reachable not just from within the backend host, but from all of your + # callers. When its value is "http://localhost:7007", it's strictly private + # and can't be reached by others. baseUrl: ${BACKSTAGE_BASE_URL} cors: origin: ${BACKSTAGE_BASE_URL} + # The listener can also be expressed as a single : string. In this case we bind to + # all interfaces, the most permissive setting. The right value depends on your specific deployment. + # listen: ':7007' listen: port: 7007 auth: - autologout: - enabled: true environment: production providers: + # See https://backstage.io/docs/auth/github/provider github: production: - clientId: ${GITHUB_AUTH_PROVIDER_CLIENT_ID} - clientSecret: ${GITHUB_AUTH_PROVIDER_CLIENT_SECRET} + clientId: ${AUTH_GITHUB_CLIENT_ID} + clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} + ## uncomment if using GitHub Enterprise + # enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} + signIn: + resolvers: + - resolver: usernameMatchingUserEntityName catalog: locations: [] diff --git a/app-config.yaml b/app-config.yaml index 72f1d07..b8401ea 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -9,13 +9,17 @@ backend: baseUrl: http://localhost:7007 listen: port: 7007 + # Uncomment the following host directive to bind to specific interfaces + # host: 127.0.0.1 csp: - # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference connect-src: ["'self'", 'http:', 'https:'] + # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference + # Default Helmet Content-Security-Policy values can be removed by setting the key to false cors: origin: http://localhost:3000 methods: [GET, HEAD, PATCH, POST, PUT, DELETE] credentials: true + # Config options: https://node-postgres.com/apis/client database: client: pg connection: @@ -29,11 +33,29 @@ backend: # ssl: # ca: # if you have a CA file and want to verify it you can uncomment this section # $file: /ca/server.crt + # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir + # reading: + # allow: + # - host: "*.swagger.io" integrations: github: - host: github.com + # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information + # about setting up the GitHub integration here: https://backstage.io/docs/integrations/github/locations#configuration token: ${GITHUB_TOKEN} + ### Example for how to add your GitHub Enterprise instance using the API: + # - host: ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: ${GHE_TOKEN} + +proxy: + ### Example for how to add a proxy endpoint for the frontend. + ### A typical reason to do this is to handle HTTPS and CORS for internal services. + # endpoints: + # '/test': + # target: 'https://example.com' + # changeOrigin: true # Reference documentation http://backstage.io/docs/features/techdocs/configuration # Note: After experimenting with basic setup, use CI/CD to generate docs @@ -47,19 +69,36 @@ techdocs: type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. auth: + # See https://backstage.io/docs/auth/autologout autologout: enabled: false + idleTimeoutMinutes: 60 + promptBeforeIdleSeconds: 10 + useWorkerTimers: true + logoutIfDisconnected: true environment: development + # See https://backstage.io/docs/auth providers: + # See https://backstage.io/docs/auth/github/provider github: development: - clientId: ${GITHUB_AUTH_PROVIDER_CLIENT_ID} - clientSecret: ${GITHUB_AUTH_PROVIDER_CLIENT_SECRET} + clientId: ${AUTH_GITHUB_CLIENT_ID} + clientSecret: ${AUTH_GITHUB_CLIENT_SECRET} + ## uncomment if using GitHub Enterprise + # enterpriseInstanceUrl: ${AUTH_GITHUB_ENTERPRISE_INSTANCE_URL} + signIn: + resolvers: + - resolver: usernameMatchingUserEntityName + # - resolver: emailMatchingUserEntityProfileEmail + # - resolver: emailLocalPartMatchingUserEntityName + +enableExperimentalRedirectFlow: false scaffolder: + # see https://backstage.io/docs/features/software-templates/configuration for software template options defaultAuthor: name: Kadras Developer Portal - email: scaffolder@backstage.io + email: scaffolder@kadras.io defaultCommitMessage: 'Initial commit' catalog: @@ -67,18 +106,33 @@ catalog: entityFilename: catalog-info.yaml pullRequestBranchName: backstage-integration rules: - - allow: [Component, System, API, Resource, Location] + - allow: [API, Component, Location, Resource, System] locations: - ## Uncomment these lines to add more example data - # - type: url - # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml - - - type: url - target: https://github.com/ThomasVitale/symphony-for-dev-and-platform/blob/main/platform/catalog/organization/catalog-info.yml + # Users and Groups + - type: file + target: ../../demo-catalog/organizations/organization-kadras.yml + rules: + - allow: [Group, Location, User] + - type: file + target: ../../demo-catalog/organizations/organization-guest.yml rules: - - allow: [User, Group, Location] + - allow: [Group, User] - - type: url - target: https://github.com/ThomasVitale/symphony-for-dev-and-platform/blob/main/platform/catalog/templates/catalog-info.yml + # Templates + - type: file + target: ../../demo-catalog/templates/templates.yml rules: - - allow: [Template, Location] + - allow: [Location, Template] + +# see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options +kubernetes: + serviceLocatorMethod: + type: 'multiTenant' + clusterLocatorMethods: + - type: 'config' + clusters: [] + +# see https://backstage.io/docs/permissions/getting-started for more on the permission framework +permission: + # setting this to `false` will disable permissions + enabled: true diff --git a/backstage.json b/backstage.json index dd15498..230d39d 100644 --- a/backstage.json +++ b/backstage.json @@ -1,3 +1,3 @@ { - "version": "1.23.4" + "version": "1.30.0" } diff --git a/docker-compose.yml b/compose.yml similarity index 70% rename from docker-compose.yml rename to compose.yml index 5512294..8dc845e 100644 --- a/docker-compose.yml +++ b/compose.yml @@ -1,8 +1,7 @@ -version: "3.8" services: postgres: - image: postgres:16.2-alpine + image: docker.io/library/postgres:16.4-alpine container_name: "postgres" ports: - 5432:5432 @@ -13,15 +12,16 @@ services: backstage: image: backstage container_name: "backstage" + depends_on: + - postgres ports: - 7007:7007 environment: - BACKSTAGE_BASE_URL=http://localhost:7007 - - BACKSTAGE_BACKEND_SECRET=lYXoL2XuDhIJE07CMTOunVB0pdZNCgS2 - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - POSTGRES_USER=user - POSTGRES_PASSWORD=password - GITHUB_TOKEN - - GITHUB_AUTH_PROVIDER_CLIENT_ID - - GITHUB_AUTH_PROVIDER_CLIENT_SECRET + - AUTH_GITHUB_CLIENT_ID + - AUTH_GITHUB_CLIENT_SECRET diff --git a/demo-catalog/organizations/organization-guest.yml b/demo-catalog/organizations/organization-guest.yml new file mode 100644 index 0000000..856369d --- /dev/null +++ b/demo-catalog/organizations/organization-guest.yml @@ -0,0 +1,16 @@ +--- +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: guests +spec: + type: team + children: [] + +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: guest +spec: + memberOf: [guests] diff --git a/demo-catalog/organizations/organization-kadras.yml b/demo-catalog/organizations/organization-kadras.yml new file mode 100644 index 0000000..5a42224 --- /dev/null +++ b/demo-catalog/organizations/organization-kadras.yml @@ -0,0 +1,32 @@ +--- +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: kadras + title: Kadras + description: Researching technology to power your path from idea to production on Kubernetes + links: + - url: https://www.kadras.io + title: Website + - url: https://github.com/kadras-io + title: GitHub + icon: github +spec: + type: organization + profile: + displayName: Kadras + picture: https://avatars.githubusercontent.com/u/122495299?s=400&u=b7253f864937e362b42c0b390da51ea9492194f4&v=4 + children: [] + +--- +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: ThomasVitale + title: Thomas Vitale +spec: + memberOf: [kadras] + profile: + displayName: Thomas Vitale + email: ThomasVitale@users.noreply.github.com + picture: https://avatars.githubusercontent.com/u/8523418?s=400&u=0ff9935d7aef1e1a735c661da9703c85513320ca&v=4 diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/.github/workflows/commit-stage.yml b/demo-catalog/templates/spring-boot-ai-rag/base/.github/workflows/commit-stage.yml new file mode 100644 index 0000000..035a83e --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/.github/workflows/commit-stage.yml @@ -0,0 +1,47 @@ +name: Commit Stage +on: + push: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ values.repoUrl.owner | lower }}/${{ values.repoUrl.repo }} + VERSION: ${{ '${{ github.sha }}' }} + +jobs: + build: + name: Build + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - name: Check out source code + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: 22 + distribution: temurin + cache: gradle + + - name: Compile and test + run: ./gradlew build + + - name: Package as OCI image + run: ./gradlew bootBuildImage --imageName ${{ '${{ env.REGISTRY }}' }}/${{ '${{ env.IMAGE_NAME }}' }}:${{ '${{ env.VERSION }}' }} + + - name: Authenticate with the container registry + uses: docker/login-action@v3 + with: + username: ${{ '${{ github.actor }}' }} + password: ${{ '${{ secrets.GITHUB_TOKEN }}' }} + registry: ${{ '${{ env.REGISTRY }}' }} + + - name: Publish OCI image + run: | + docker tag ${{ '${{ env.REGISTRY }}' }}/${{ '${{ env.IMAGE_NAME }}' }}:${{ '${{ env.VERSION }}' }} ${{ '${{ env.REGISTRY }}' }}/${{ '${{ env.IMAGE_NAME }}' }}:latest + docker push ${{ '${{ env.REGISTRY }}' }}/${{ '${{ env.IMAGE_NAME }}' }}:${{ '${{ env.VERSION }}' }} + docker push ${{ '${{ env.REGISTRY }}' }}/${{ '${{ env.IMAGE_NAME }}' }}:latest diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/.gitignore b/demo-catalog/templates/spring-boot-ai-rag/base/.gitignore new file mode 100644 index 0000000..1690322 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/.gitignore @@ -0,0 +1,59 @@ +### Gradle ### +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### macOS ### +.DS_Store +*.DS_Store +**/.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/.sdkmanrc b/demo-catalog/templates/spring-boot-ai-rag/base/.sdkmanrc new file mode 100644 index 0000000..9eeeeb8 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/.sdkmanrc @@ -0,0 +1,6 @@ +# Use sdkman to run "sdk env" to initialize with correct JDK version +# Enable auto-env through the sdkman_auto_env config +# See https://sdkman.io/usage#config +# A summary is to add the following to ~/.sdkman/etc/config +# sdkman_auto_env=true +java=22-graal diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/LICENSE b/demo-catalog/templates/spring-boot-ai-rag/base/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/README.md b/demo-catalog/templates/spring-boot-ai-rag/base/README.md new file mode 100644 index 0000000..9ed27f9 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/README.md @@ -0,0 +1,22 @@ +# ${{ values.name }} + +${{ values.description }} + +## Running the application + +```shell +./gradlew bootTestRun +``` + +## Calling the application + +LLM-powered application with RAG capabilities. +This example uses [httpie](https://httpie.io) to send HTTP requests. + +```shell +http --raw "Who is the lead singer?" :8080/chat +``` + +```shell +http --raw "What instrument does Clara play" :8080/chat +``` diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/build.gradle b/demo-catalog/templates/spring-boot-ai-rag/base/build.gradle new file mode 100644 index 0000000..f20055e --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/build.gradle @@ -0,0 +1,85 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.0-M2' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = '${{ values.groupId }}' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(22) + } +} + +repositories { + mavenCentral() +} + +repositories { + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } +} + +ext { + set('springAiVersion', "1.0.0-M2") +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' + + implementation 'io.micrometer:micrometer-tracing-bridge-otel' + implementation 'io.opentelemetry:opentelemetry-exporter-otlp' + implementation 'io.micrometer:micrometer-registry-otlp' + + {% if values.llmProvider == "mistral-ai" %} + implementation 'org.springframework.ai:spring-ai-mistral-ai-spring-boot-starter' + {% endif %} + {% if values.llmProvider == "ollama" %} + implementation 'org.springframework.ai:spring-ai-ollama-spring-boot-starter' + testImplementation 'org.testcontainers:ollama' + {% endif %} + {% if values.llmProvider == "openai" %} + implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter' + {% endif %} + {% if values.vectorStore == "chroma" %} + implementation 'org.springframework.ai:spring-ai-chroma-store-spring-boot-starter' + testImplementation 'org.testcontainers:chromadb' + {% endif %} + {% if values.vectorStore == "postgresql" %} + implementation 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter' + testImplementation 'org.testcontainers:postgresql' + {% endif %} + {% if values.vectorStore == "weaviate" %} + implementation 'org.springframework.ai:spring-ai-weaviate-store-spring-boot-starter' + testImplementation 'org.testcontainers:weaviate' + {% endif %} + implementation "org.springframework.ai:spring-ai-spring-cloud-bindings" + + testAndDevelopmentOnly 'org.springframework.boot:spring-boot-devtools' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation 'org.springframework.ai:spring-ai-spring-boot-testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:grafana' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}" + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +tasks.named('bootBuildImage') { + builder = "paketobuildpacks/builder-jammy-buildpackless-tiny" + buildpacks = [ "gcr.io/paketo-buildpacks/java" ] +} diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/config/workload.yml b/demo-catalog/templates/spring-boot-ai-rag/base/config/workload.yml new file mode 100644 index 0000000..a764369 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/config/workload.yml @@ -0,0 +1,28 @@ +--- +apiVersion: app.kadras.io/v1alpha1 +kind: Workload +metadata: + name: ${{ values.name }} + labels: + app: ${{ values.name }} +spec: + image: ghcr.io/${{ values.repoUrl.owner | lower }}/${{ values.repoUrl.repo }} + application: + framework: spring-boot + language: java + serviceClaims: + {%- if values.llmProvider == "mistral" %} + - name: mistral-ai + {%- endif %} + {%- if values.llmProvider == "openai" %} + - name: open-ai + {%- endif %} + {%- if values.vectorStore == "chroma" %} + - name: chroma + {%- endif %} + {%- if values.vectorStore == "postgresql" %} + - name: postgresql + {%- endif %} + {%- if values.vectorStore == "weaviate" %} + - name: weaviate + {%- endif %} diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/gradle/wrapper/gradle-wrapper.jar b/demo-catalog/templates/spring-boot-ai-rag/base/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/demo-catalog/templates/spring-boot-ai-rag/base/gradle/wrapper/gradle-wrapper.jar differ diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/gradle/wrapper/gradle-wrapper.properties b/demo-catalog/templates/spring-boot-ai-rag/base/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9355b41 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/gradlew b/demo-catalog/templates/spring-boot-ai-rag/base/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/gradlew.bat b/demo-catalog/templates/spring-boot-ai-rag/base/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/settings.gradle b/demo-catalog/templates/spring-boot-ai-rag/base/settings.gradle new file mode 100644 index 0000000..cbd6fa7 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/settings.gradle @@ -0,0 +1,7 @@ +pluginManagement { + repositories { + maven { url 'https://repo.spring.io/milestone' } + gradlePluginPortal() + } +} +rootProject.name = '${{ values.artifactId }}' diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/src/main/java/${{values.basePackageDir}}/DemoApplication.java b/demo-catalog/templates/spring-boot-ai-rag/base/src/main/java/${{values.basePackageDir}}/DemoApplication.java new file mode 100644 index 0000000..e05c998 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/src/main/java/${{values.basePackageDir}}/DemoApplication.java @@ -0,0 +1,102 @@ +package ${{ values.basePackage }}; + +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; +import org.springframework.ai.reader.TextReader; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.client.ClientHttpRequestFactories; +import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Duration; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} + +@RestController +class ChatController { + + private final ChatClient chatClient; + private final VectorStore vectorStore; + + ChatController(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) { + this.chatClient = chatClientBuilder.build(); + this.vectorStore = vectorStore; + } + + @PostMapping("/chat") + String chatWithDocument(@RequestBody String message) { + return chatClient.prompt() + .advisors(new QuestionAnswerAdvisor(vectorStore)) + .user(message) + .call() + .content(); + } + +} + +@Component +class IngestionPipeline { + + private static final Logger logger = LoggerFactory.getLogger(IngestionPipeline.class); + private final JdbcClient jdbcClient; + private final VectorStore vectorStore; + + @Value("classpath:documents/story.md") + Resource textFile; + + public IngestionPipeline(JdbcClient jdbcClient, VectorStore vectorStore) { + this.jdbcClient = jdbcClient; + this.vectorStore = vectorStore; + } + + @PostConstruct + public void run() { + if ((long) jdbcClient.sql("select count(*) from vector_store").query().singleValue() > 0) { + return; + } + + logger.info("Loading text files as Documents"); + var documents = new TextReader(textFile).get(); + + logger.info("Creating and storing Embeddings from Documents"); + vectorStore.add(new TokenTextSplitter().split(documents)); + } + +} + +@Configuration(proxyBeanMethods = false) +class HttpClientAutoConfiguration { + + @Bean + RestClientCustomizer restClientCustomizer() { + return restClientBuilder -> { + restClientBuilder + .requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS + .withConnectTimeout(Duration.ofSeconds(120)) + .withReadTimeout(Duration.ofSeconds(120)))); + }; + } + +} diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/src/main/resources/application.yml b/demo-catalog/templates/spring-boot-ai-rag/base/src/main/resources/application.yml new file mode 100644 index 0000000..7b6011e --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/src/main/resources/application.yml @@ -0,0 +1,66 @@ +spring: + application: + name: ${{ values.artifactId }} + ai: + chat: + observations: + include-completion: true + include-prompt: true + image: + observations: + include-prompt: true + {%- if values.llmProvider == "mistral-ai" %} + mistralai: + chat: + options: + model: mistral-small-latest + temperature: 0.7 + embedding: + options: + model: mistral-embed + {%- endif %} + {%- if values.llmProvider == "ollama" %} + ollama: + chat: + options: + model: mistral + num-ctx: 4096 + temperature: 0.7 + embedding: + options: + model: nomic-embed-text + {%- endif %} + {%- if values.llmProvider == "openai" %} + openai: + chat: + options: + model: gpt-4o-mini + temperature: 0.7 + embedding: + options: + model: text-embedding-3-small + image: + options: + model: dall-e-3 + {%- endif %} + vectorstore: + {%- if values.vectorStore == "postgresql" %} + pgvector: + {%- if values.llmProvider == "ollama" %} + dimensions: 768 + {%- endif %} + index-type: hnsw + initialize-schema: true + {%- endif %} + +management: + endpoints: + web: + exposure: + include: "*" + metrics: + tags: + service.name: ${spring.application.name} + tracing: + sampling: + probability: 1.0 diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/src/main/resources/documents/story.md b/demo-catalog/templates/spring-boot-ai-rag/base/src/main/resources/documents/story.md new file mode 100644 index 0000000..560fd01 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/src/main/resources/documents/story.md @@ -0,0 +1,23 @@ +# The Story of The Dukes + +It was a chilly autumn night when The Dukes took to the stage at the infamous "Rockin' Roasters" venue in downtown Seattle. +The air was alive with anticipation as the crowd eagerly awaited the debut performance of this talented new band. And what +a lineup they had - three guitarists, a bass player, a pianist, a singer, and a drummer, each one chosen for their exceptional +skill and passion for music. + +Leading the charge was guitarist "Dark Roast" Dave, whose rich, full-bodied riffs set the tone for the evening. On his right +was "French Vanilla" Fiona, delivering crisp, clean guitar lines with ease. To their left was "Latte" Larry, whose intricate +fingerpicking added a delicate touch to the band's sound. + +Bassist "Espresso" Esther provided the heartbeat of The Dukes, her driving rhythms pulsing through the speakers and keeping +the crowd moving. Next to her was "Cappuccino" Clara, whose soaring keyboard melodies added a layer of depth and complexity +to the music. + +Lead singer "Mocha" Mike brought it all together with his powerful, soulful voice, weaving in and out of the instruments +with ease. And behind it all was "Chai" Charlie, the drummer, keeping the whole thing together with a steady beat that never +faltered. + +As they launched into their debut single, "The Roasted Riff," the crowd erupted in cheers and applause, captivated by the +sheer talent and energy of The Dukes. It was clear that this was a band to watch, one that would leave its mark on the music +world. And as they continued to play, it became increasingly evident that their sound was something truly special - a perfect +blend of coffee-inspired flavors and rock 'n' roll spirit. diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/DemoApplicationTests.java b/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/DemoApplicationTests.java new file mode 100644 index 0000000..60dc23f --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/DemoApplicationTests.java @@ -0,0 +1,15 @@ +package ${{ values.basePackage }}; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@Disabled +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/TestDemoApplication.java b/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/TestDemoApplication.java new file mode 100644 index 0000000..4fb16ad --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/TestDemoApplication.java @@ -0,0 +1,11 @@ +package ${{ values.basePackage }}; + +import org.springframework.boot.SpringApplication; + +public class TestDemoApplication { + + public static void main(String[] args) { + SpringApplication.from(DemoApplication::main).with(TestcontainersConfiguration.class).run(args); + } + +} diff --git a/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/TestcontainersConfiguration.java b/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/TestcontainersConfiguration.java new file mode 100644 index 0000000..1967f1c --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/base/src/test/java/${{values.basePackageDir}}/TestcontainersConfiguration.java @@ -0,0 +1,51 @@ +package ${{ values.basePackage }}; + +import org.springframework.boot.devtools.restart.RestartScope; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +{%- if values.vectorStore == "postgresql" %} +import org.testcontainers.containers.PostgreSQLContainer; +{%- endif %} +import org.testcontainers.grafana.LgtmStackContainer; +{% if values.llmProvider == "ollama" %} +import org.testcontainers.ollama.OllamaContainer; +{%- endif %} +import org.testcontainers.utility.DockerImageName; + +import java.time.Duration; + +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersConfiguration { + + {% if values.llmProvider == "ollama" %} + @Bean + @RestartScope + @ServiceConnection + @Profile("ollama-image") + OllamaContainer ollama() { + return new OllamaContainer(DockerImageName.parse("ghcr.io/thomasvitale/ollama-mistral") + .asCompatibleSubstituteFor("ollama/ollama")); + } + {%- endif %} + + {% if values.vectorStore == "postgresql" %} + @Bean + @RestartScope + @ServiceConnection + PostgreSQLContainer postgresContainer() { + return new PostgreSQLContainer<>(DockerImageName.parse("pgvector/pgvector:pg16")); + } + {%- endif %} + + @Bean + @RestartScope + @ServiceConnection + LgtmStackContainer lgtmContainer() { + return new LgtmStackContainer("grafana/otel-lgtm:0.7.1") + .withStartupTimeout(Duration.ofMinutes(2)) + .withReuse(true); + } + +} diff --git a/demo-catalog/templates/spring-boot-ai-rag/catalog-info/catalog-info.yml b/demo-catalog/templates/spring-boot-ai-rag/catalog-info/catalog-info.yml new file mode 100644 index 0000000..3251d11 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/catalog-info/catalog-info.yml @@ -0,0 +1,13 @@ +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: ${{ values.name }} + description: ${{ values.description }} + annotations: + backstage.io/techdocs-ref: dir:. + github.com/project-slug: ${{ values.repoUrl.owner }}/${{ values.repoUrl.repo }} +spec: + type: ${{ values.applicationType }} + lifecycle: ${{ values.lifecycle }} + owner: ${{ values.owner }} diff --git a/demo-catalog/templates/spring-boot-ai-rag/template.yml b/demo-catalog/templates/spring-boot-ai-rag/template.yml new file mode 100644 index 0000000..39567b8 --- /dev/null +++ b/demo-catalog/templates/spring-boot-ai-rag/template.yml @@ -0,0 +1,205 @@ +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: spring-ai-rag + title: Spring AI RAG + description: Spring Boot using Java and Gradle with Spring AI integration to build LLM-powered applications with RAG. + annotations: + backstage.io/time-saved: PT12H + tags: + - recommended + - java + - spring + - ai +spec: + owner: kadras + type: service + + parameters: + - title: Application Information + required: + - name + - description + - owner + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + ui:field: EntityNamePicker + maxLength: 63 + description: + title: Description + type: string + description: Short explanation of what this service is for + ui:options: + rows: 5 + default: LLM-powered application with RAG capabilities. + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + catalogFilter: + kind: [Group, User] + + - title: Application Repository Information + required: + - repoUrl + - repoBranch + properties: + repoUrl: + title: Repository Location + type: string + ui:autofocus: true + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com + repoBranch: + title: Repository Default Branch + type: string + default: main + + - title: Java Project Metadata + required: + - groupId + - artifactId + - basePackage + properties: + groupId: + title: Group + type: string + description: Maven Group ID (e.g. io.kadras) + ui:autofocus: true + default: io.kadras + artifactId: + title: Artifact + type: string + description: Maven Artifact ID (e.g. demo) + default: demo + basePackage: + title: Base Package + type: string + description: Base name for the Java package (e.g. io.kadras.demo) + default: io.kadras.demo + + - title: Application Dependencies + required: + - llmProvider + properties: + llmProvider: + title: LLM Provider + type: string + description: Choose the large language model provider to integrate with + ui:autofocus: true + default: mistral-ai + enum: + - mistral-ai + - ollama + - openai + enumNames: + - "Mistral AI" + - "Ollama" + - "OpenAI" + vectorStore: + title: Vector Store + type: string + description: Choose the vector store to integrate with + default: postgresql + enum: + - chroma + - postgresql + - weaviate + enumNames: + - "ChromaDB" + - "PostgreSQL" + - "Weaviate" + + - title: Deployment Information + required: + - workflowEngine + properties: + workflowEngine: + title: Workflow Engine + type: string + description: Choose the workflow engine to use for setting up pipelines + ui:autofocus: true + default: githubActions + enum: + - githubActions + - tektonPipelines + enumNames: + - "GitHub Actions" + - "Tekton Pipelines" + + steps: + - id: fetch-base + name: Fetch Base + action: fetch:template + input: + url: ./base + values: + name: ${{ parameters.name }} + description: ${{ parameters.description }} + repoUrl: ${{ parameters.repoUrl | parseRepoUrl }} + groupId: ${{ parameters.groupId }} + artifactId: ${{ parameters.artifactId }} + basePackage: ${{ parameters.basePackage }} + basePackageDir: ${{ parameters.basePackage | replace(".", "/") }} + llmProvider: ${{ parameters.llmProvider }} + vectorStore: ${{ parameters.vectorStore }} + + - id: fetch-catalog-info + name: Fetch Catalog Info + action: fetch:template + input: + url: ./catalog-info/ + values: + name: ${{ parameters.name }} + description: ${{ parameters.description }} + owner: ${{ parameters.owner }} + repoUrl: ${{ parameters.repoUrl | parseRepoUrl }} + applicationType: service + lifecycle: experimental + + - id: publish + name: Publish Application Repository + action: publish:github + input: + repoUrl: ${{ parameters.repoUrl }} + allowedHosts: ['github.com'] + defaultBranch: ${{ parameters.repoBranch }} + description: ${{ parameters.description }} + gitAuthorName: ${{ user.entity.spec.profile.displayName }} + gitAuthorEmail: ${{ user.entity.spec.profile.email }} + repoVisibility: private + topics: + - java + - spring-boot + - spring-ai + - ai + - large-language-models + - retrieval-augmented-generation + + - id: register + name: Register Catalog Entity + action: catalog:register + input: + repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }} + catalogInfoPath: "/catalog-info.yml" + + output: + links: + - title: Open Application Repository + url: ${{ steps['publish'].output.remoteUrl }} + - title: Open Application in Portal + icon: catalog + entityRef: ${{ steps['register'].output.entityRef }} + text: + - title: Getting Started + content: | + 1. `git clone ${{ steps['publish'].output.remoteUrl }}` + 2. `./gradlew bootTestRun` diff --git a/demo-catalog/templates/templates.yml b/demo-catalog/templates/templates.yml new file mode 100644 index 0000000..cfa39a6 --- /dev/null +++ b/demo-catalog/templates/templates.yml @@ -0,0 +1,9 @@ +--- +apiVersion: backstage.io/v1alpha1 +kind: Location +metadata: + name: templates + description: A collection of Templates +spec: + targets: + - ./spring-boot-ai-rag/template.yml diff --git a/package.json b/package.json index ee54603..20320b8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "node": "18 || 20" }, "scripts": { - "dev": "concurrently \"yarn start\" \"yarn start-backend\"", + "dev": "concurrently \"yarn start\" \"yarn dotenv -e .env yarn start-backend\"", "start": "yarn workspace app start", "start-backend": "yarn workspace backend start", "build:backend": "yarn workspace backend build", @@ -22,8 +22,7 @@ "lint": "backstage-cli repo lint --since origin/main", "lint:all": "backstage-cli repo lint", "prettier:check": "prettier --check .", - "new": "backstage-cli new --scope internal", - "ci": "yarn run lint:all build:all test:all" + "new": "backstage-cli new --scope internal" }, "workspaces": { "packages": [ @@ -32,15 +31,15 @@ ] }, "devDependencies": { - "@backstage/cli": "^0.25.2", + "@backstage/cli": "^0.27.0", "@backstage/e2e-test-utils": "^0.1.1", "@playwright/test": "^1.32.3", "@spotify/prettier-config": "^12.0.0", "concurrently": "^8.0.0", "lerna": "^7.3.0", - "node-gyp": "^9.0.0", + "node-gyp": "^10.0.0", "prettier": "^2.3.2", - "typescript": "~5.3.0" + "typescript": "~5.4.0" }, "resolutions": { "@types/react": "^18", diff --git a/packages/app/e2e-tests/app.test.ts b/packages/app/e2e-tests/app.test.ts index d45bc0d..839ff88 100644 --- a/packages/app/e2e-tests/app.test.ts +++ b/packages/app/e2e-tests/app.test.ts @@ -19,5 +19,9 @@ import { test, expect } from '@playwright/test'; test('App should render the welcome page', async ({ page }) => { await page.goto('/'); + const enterButton = page.getByRole('button', { name: 'Enter' }); + await expect(enterButton).toBeVisible(); + await enterButton.click(); + await expect(page.getByText('My Company Catalog')).toBeVisible(); }); diff --git a/packages/app/package.json b/packages/app/package.json index 61594f9..ca74c35 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -14,32 +14,32 @@ "lint": "backstage-cli package lint" }, "dependencies": { - "@backstage/app-defaults": "^1.5.0", - "@backstage/catalog-model": "^1.4.4", - "@backstage/cli": "^0.25.2", - "@backstage/core-app-api": "^1.12.0", - "@backstage/core-components": "^0.14.0", - "@backstage/core-plugin-api": "^1.9.0", - "@backstage/integration-react": "^1.1.24", - "@backstage/plugin-api-docs": "^0.11.0", - "@backstage/plugin-catalog": "^1.17.0", - "@backstage/plugin-catalog-common": "^1.0.21", - "@backstage/plugin-catalog-graph": "^0.4.0", - "@backstage/plugin-catalog-import": "^0.10.6", - "@backstage/plugin-catalog-react": "^1.10.0", - "@backstage/plugin-github-actions": "^0.6.11", - "@backstage/plugin-home": "^0.6.2", - "@backstage/plugin-org": "^0.6.20", - "@backstage/plugin-permission-react": "^0.4.20", - "@backstage/plugin-scaffolder": "^1.18.0", - "@backstage/plugin-search": "^1.4.6", - "@backstage/plugin-search-react": "^1.7.6", - "@backstage/plugin-tech-radar": "^0.6.13", - "@backstage/plugin-techdocs": "^1.10.0", - "@backstage/plugin-techdocs-module-addons-contrib": "^1.1.5", - "@backstage/plugin-techdocs-react": "^1.1.16", - "@backstage/plugin-user-settings": "^0.8.1", - "@backstage/theme": "^0.5.1", + "@backstage-community/plugin-github-actions": "^0.6.22", + "@backstage/app-defaults": "^1.5.10", + "@backstage/catalog-model": "^1.6.0", + "@backstage/cli": "^0.27.0", + "@backstage/core-app-api": "^1.14.2", + "@backstage/core-components": "^0.14.10", + "@backstage/core-plugin-api": "^1.9.3", + "@backstage/integration-react": "^1.1.30", + "@backstage/plugin-api-docs": "^0.11.8", + "@backstage/plugin-catalog": "^1.22.0", + "@backstage/plugin-catalog-common": "^1.0.26", + "@backstage/plugin-catalog-graph": "^0.4.8", + "@backstage/plugin-catalog-import": "^0.12.2", + "@backstage/plugin-catalog-react": "^1.12.3", + "@backstage/plugin-home": "^0.7.9", + "@backstage/plugin-kubernetes": "^0.11.13", + "@backstage/plugin-org": "^0.6.28", + "@backstage/plugin-permission-react": "^0.4.25", + "@backstage/plugin-scaffolder": "^1.24.0", + "@backstage/plugin-search": "^1.4.15", + "@backstage/plugin-search-react": "^1.7.14", + "@backstage/plugin-techdocs": "^1.10.8", + "@backstage/plugin-techdocs-module-addons-contrib": "^1.1.13", + "@backstage/plugin-techdocs-react": "^1.2.7", + "@backstage/plugin-user-settings": "^0.8.11", + "@backstage/theme": "^0.5.6", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "history": "^5.0.0", @@ -50,7 +50,7 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/test-utils": "^1.5.0", + "@backstage/test-utils": "^1.5.10", "@playwright/test": "^1.32.3", "@testing-library/dom": "^9.0.0", "@testing-library/jest-dom": "^6.0.0", diff --git a/packages/app/public/index.html b/packages/app/public/index.html index 3337729..18da7c4 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -6,7 +6,7 @@