diff --git a/.gitea/workflows/deploy-branch-cleanup.yaml b/.gitea/workflows/deploy-branch-cleanup.yaml new file mode 100644 index 0000000000..764e04ccf2 --- /dev/null +++ b/.gitea/workflows/deploy-branch-cleanup.yaml @@ -0,0 +1,14 @@ +name: Remove branch deployment from branch.dsv.su.se +on: + pull_request: + types: + - closed +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - uses: https://gitea.dsv.su.se/ansv7779/action-branch-deploy@v2 + with: + cleanup-ssh-key: ${{ secrets.BRANCH_CLEANUP_KEY }} + compose-file: compose-branch-deploy.yaml + mode: 'cleanup' diff --git a/.gitea/workflows/deploy-branch.yaml b/.gitea/workflows/deploy-branch.yaml new file mode 100644 index 0000000000..73ab386992 --- /dev/null +++ b/.gitea/workflows/deploy-branch.yaml @@ -0,0 +1,26 @@ +name: Deploy to branch.dsv.su.se +on: + - pull_request +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - id: deploy + uses: https://gitea.dsv.su.se/ansv7779/action-branch-deploy@v2 + with: + ssh-key: ${{ secrets.BRANCH_DEPLOY_KEY }} + compose-file: compose-branch-deploy.yaml + - name: Post URL to deployment as comment + uses: actions/github-script@v7 + if: github.event.action == 'opened' + env: + BRANCH_URL: ${{ steps.deploy.outputs.url }} + with: + script: | + const url = process.env.BRANCH_URL; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `Deployed to ${url}` + }) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..9212410a58 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM debian:bookworm AS build + +RUN apt-get update && apt-get install -y openjdk-17-jdk-headless + +WORKDIR /app + +COPY pom.xml . +COPY .mvn/ .mvn/ +COPY mvnw . +COPY api/pom.xml api/pom.xml +COPY core/pom.xml core/pom.xml +COPY view/pom.xml view/pom.xml +COPY war/pom.xml war/pom.xml +COPY daisy-integration/pom.xml daisy-integration/pom.xml + +# Download dependencies in a separate layer to allow caching for future builds +RUN ./mvnw dependency:go-offline \ + --batch-mode \ + --define includeScope=compile \ + --activate-profiles docker-dependencies + +COPY api/src/ api/src/ +COPY core/src/ core/src/ +COPY view/src/ view/src/ +COPY war/src/ war/src/ +COPY daisy-integration/src/ daisy-integration/src/ + +RUN ./mvnw package \ + --offline \ + --define skipTests \ + --activate-profiles branch,DEV \ + --define skip.npm \ + --define skip.installnodenpm + +FROM tomcat:10 AS run + +COPY --from=build /app/war/target/*.war /usr/local/tomcat/webapps/ROOT.war + +EXPOSE 8080 diff --git a/README.md b/README.md index 22533e79fc..de348d2c3b 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,13 @@ can be performed. Go to `Settings -> Language & Frameworks -> JavaScript -> Prettier` and then check `Automatic Prettier Configuration`, set `Run for files` to `**/*.{java}`, and finally check `Run on save`. + +## Test servers +All pull requests are automatically deployed to a test server. +The URL to the test server will be posted as a comment in the pull request once deployed. + +Prepare test data in the `DataInitializer` class to help others test your changes. +Document (in the pull request) which users to log in as and what to do to see the changes. + +If you want to reset the data to its original state you can re-run the "deploy-branch.yaml" +workflow at https://gitea.dsv.su.se/DMC/scipro/actions for the branch you want to reset. diff --git a/compose-branch-deploy.yaml b/compose-branch-deploy.yaml new file mode 100644 index 0000000000..aba04bbb51 --- /dev/null +++ b/compose-branch-deploy.yaml @@ -0,0 +1,72 @@ +services: + scipro: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + depends_on: + db: + condition: service_healthy + oauth2: + condition: service_started + environment: + - JDBC_DATABASE_URL=jdbc:mariadb://db:3306/scipro + - JDBC_DATABASE_USERNAME=scipro + - JDBC_DATABASE_PASSWORD=scipro + - OAUTH2_AUTHORIZATION_URI=https://oauth2-${VHOST}/authorize + - OAUTH2_TOKEN_URI=https://oauth2-${VHOST}/exchange + - OAUTH2_USER_INFO_URI=https://oauth2-${VHOST}/verify + - OAUTH2_CLIENT_ID=scipro_client + - OAUTH2_CLIENT_SECRET=scipro_secret + - OAUTH2_RESOURCE_SERVER_ID=scipro_api_client + - OAUTH2_RESOURCE_SERVER_SECRET=scipro_api_secret + - OAUTH2_RESOURCE_SERVER_INTROSPECTION_URI=https://oauth2-${VHOST}/introspect + networks: + - traefik + - internal + labels: + - "traefik.enable=true" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${VHOST}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt" + + db: + image: mariadb:10.11 + restart: unless-stopped + networks: + - internal + environment: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: scipro + MARIADB_USER: scipro + MARIADB_PASSWORD: scipro + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect"] + start_period: 10s + interval: 10s + timeout: 5s + retries: 6 + + oauth2: + build: + context: https://github.com/dsv-su/toker.git + dockerfile: embedded.Dockerfile + restart: unless-stopped + environment: + - CLIENT_ID=scipro_client + - CLIENT_SECRET=scipro_secret + - CLIENT_REDIRECT_URI=https://${VHOST}/login/oauth2/code/scipro + - RESOURCE_SERVER_ID=scipro_api_client + - RESOURCE_SERVER_SECRET=scipro_api_secret + networks: + - traefik + labels: + - "traefik.enable=true" + - "traefik.http.routers.oauth2-${COMPOSE_PROJECT_NAME}.rule=Host(`oauth2-${VHOST}`)" + - "traefik.http.routers.oauth2-${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt" + +networks: + traefik: + name: traefik + external: true + internal: + name: ${COMPOSE_PROJECT_NAME}_internal diff --git a/war/pom.xml b/war/pom.xml index 456eeae393..19e7317b23 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -12,6 +12,10 @@ <artifactId>war</artifactId> <packaging>war</packaging> + <properties> + <spring.profile.active>tomcat</spring.profile.active> + </properties> + <dependencies> <dependency> <groupId>org.springframework.boot</groupId> @@ -88,7 +92,26 @@ </dependencies> <build> + <resources> + <resource> + <directory>src/main/resources</directory> + <filtering>true</filtering> + </resource> + </resources> <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>3.3.1</version> + <configuration> + <propertiesEncoding>${project.build.sourceEncoding}</propertiesEncoding> + <delimiters> + <!-- delimiter for resource filtering is changed since Spring hijacks ${...} --> + <delimiter>@</delimiter> + </delimiters> + <useDefaultDelimiters>false</useDefaultDelimiters> + </configuration> + </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> @@ -110,4 +133,53 @@ </plugins> </build> + <profiles> + <profile> + <id>branch</id> + <properties> + <spring.profile.active>branch</spring.profile.active> + </properties> + </profile> + <profile> + <id>docker-dependencies</id> + <!-- + Some dependencies are not discovered by default when running dependency:go-offline. + They are added here manually to allow Docker build layers to be cached properly. + --> + <dependencies> + <dependency> + <groupId>org.jboss.logging</groupId> + <artifactId>jboss-logging</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml</groupId> + <artifactId>classmate</artifactId> + </dependency> + <dependency> + <groupId>net.bytebuddy</groupId> + <artifactId>byte-buddy-agent</artifactId> + </dependency> + <dependency> + <groupId>com.querydsl</groupId> + <artifactId>querydsl-apt</artifactId> + <version>${querydsl.version}</version> + <classifier>jakarta</classifier> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>net.minidev</groupId> + <artifactId>json-smart</artifactId> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-core</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + </profile> + </profiles> </project> diff --git a/war/src/main/java/se/su/dsv/scipro/war/Main.java b/war/src/main/java/se/su/dsv/scipro/war/Main.java index 2a14e64915..e47665cc3a 100644 --- a/war/src/main/java/se/su/dsv/scipro/war/Main.java +++ b/war/src/main/java/se/su/dsv/scipro/war/Main.java @@ -16,12 +16,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.orm.jpa.SharedEntityManagerCreator; import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter; +import org.springframework.web.filter.ForwardedHeaderFilter; import se.su.dsv.scipro.CoreConfig; import se.su.dsv.scipro.FileSystemStore; import se.su.dsv.scipro.RepositoryConfiguration; @@ -84,6 +87,22 @@ public class Main extends SpringBootServletInitializer implements ServletContain return currentProfile; } + /** + * Spring runs on HTTP and is protected by a HTTPS proxy. + * This filter takes the `X-Forwarded-*` headers and updates the request to reflect the original HTTP request. + * <p> + * Note: This is not needed when we're running behind Apache as a proxy since it uses an AJP connector that has a + * built-in mechanism for handling this, + * see <a href="https://tomcat.apache.org/connectors-doc/common_howto/proxy.html#AJP_as_a_Solution">AJP proxy documentation</a>. + * So this is only for the temporary test servers that are running behind Traefik and use regular HTTP. + */ + @Bean + public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() { + var filterRegistrationBean = new FilterRegistrationBean<>(new ForwardedHeaderFilter()); + filterRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); + return filterRegistrationBean; + } + @Bean public FileStore fileStore() { return new FileSystemStore(); diff --git a/war/src/main/resources/application-branch.properties b/war/src/main/resources/application-branch.properties new file mode 100644 index 0000000000..0d6cb9b213 --- /dev/null +++ b/war/src/main/resources/application-branch.properties @@ -0,0 +1,19 @@ +spring.datasource.url=${JDBC_DATABASE_URL} +spring.datasource.username=${JDBC_DATABASE_USERNAME} +spring.datasource.password=${JDBC_DATABASE_PASSWORD} + +profile=DEV + +# No secrets available for branch deployment to branch.dsv.su.se +# Will have to set up some mock API for this later +service.grading.url= +oauth.uri= +oauth.clientId= +oauth.clientSecret= +oauth.redirectUri= + +# No secrets available for branch deployment to branch.dsv.su.se +# Will have to set up some mock API for this later +daisy.api.url= +daisy.api.username= +daisy.api.password= diff --git a/war/src/main/resources/application-tomcat.properties b/war/src/main/resources/application-tomcat.properties new file mode 100644 index 0000000000..4e2b4fedce --- /dev/null +++ b/war/src/main/resources/application-tomcat.properties @@ -0,0 +1 @@ +spring.datasource.jndi-name=java:/comp/env/jdbc/sciproDS diff --git a/war/src/main/resources/application.properties b/war/src/main/resources/application.properties index f405136272..bb89287006 100644 --- a/war/src/main/resources/application.properties +++ b/war/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.datasource.jndi-name=java:/comp/env/jdbc/sciproDS +spring.profiles.active=@spring.profile.active@ spring.flyway.baseline-version=2 spring.flyway.baseline-on-migrate=true @@ -16,17 +16,17 @@ springdoc.swagger-ui.path=/swagger springdoc.swagger-ui.persist-authorization=true # These will be overwritten by configuration in the environment of servers it is deployed to -spring.security.oauth2.resourceserver.opaquetoken.client-id=scipro-api-client -spring.security.oauth2.resourceserver.opaquetoken.client-secret=scipro-api-secret -spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:59733/introspect +spring.security.oauth2.resourceserver.opaquetoken.client-id=${OAUTH2_RESOURCE_SERVER_ID:scipro-api-client} +spring.security.oauth2.resourceserver.opaquetoken.client-secret=${OAUTH2_RESOURCE_SERVER_SECRET:scipro-api-secret} +spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=${OAUTH2_RESOURCE_SERVER_INTROSPECTION_URI:http://localhost:59733/introspect} # Log in via local OAuth 2 authorization server -spring.security.oauth2.client.provider.docker.user-info-uri=http://localhost:59734/verify +spring.security.oauth2.client.provider.docker.user-info-uri=${OAUTH2_USER_INFO_URI:http://localhost:59734/verify} spring.security.oauth2.client.provider.docker.user-name-attribute=sub -spring.security.oauth2.client.provider.docker.token-uri=http://localhost:59734/exchange -spring.security.oauth2.client.provider.docker.authorization-uri=http://localhost:59734/authorize +spring.security.oauth2.client.provider.docker.token-uri=${OAUTH2_TOKEN_URI:http://localhost:59734/exchange} +spring.security.oauth2.client.provider.docker.authorization-uri=${OAUTH2_AUTHORIZATION_URI:http://localhost:59734/authorize} spring.security.oauth2.client.registration.scipro.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} spring.security.oauth2.client.registration.scipro.provider=docker -spring.security.oauth2.client.registration.scipro.client-id=scipro -spring.security.oauth2.client.registration.scipro.client-secret=s3cr3t +spring.security.oauth2.client.registration.scipro.client-id=${OAUTH2_CLIENT_ID:scipro} +spring.security.oauth2.client.registration.scipro.client-secret=${OAUTH2_CLIENT_SECRET:s3cr3t} spring.security.oauth2.client.registration.scipro.authorization-grant-type=authorization_code