hapifhir / hapi-fhir-jpaserver-starter

Apache License 2.0
394 stars 1.05k forks source link

Unable to host hapi server in sub-urls other than root #172

Open petermicuch opened 3 years ago

petermicuch commented 3 years ago

Right now using docker file as is, it is not possible to define URL other than root to host hapi-server. I.e. we can use https://host.docker.internal, but can not use https://host.docker.internal/fhir-dstu3. If you do that, you will get java scripts downloaded just fine, but all resources are returning 404 (page not found).

One option would be to modify docker file CMD part to enable this and user could then specify this using environment:

COPY --from=build-hapi /tmp/hapi-fhir-jpaserver-starter/target/*.war /usr/local/tomcat/webapps/
ENV BASE_URL "/ROOT"

EXPOSE 8080

CMD mv /usr/local/tomcat/webapps/ROOT.war /usr/local/tomcat/webapps$BASE_URL.war && catalina.sh run

Another option is to rewrite responses in your API gateway (i.e. ingress nginx) to achieve this is working.

But maybe there is better more natural way of doing this in java (i.e. embedded tomcat?) then to move war file around to mimic this behavior or force rewrites in your API gateway. And maybe this could somehow work in cooperation with "hapi.fhir.server_address" setting, otherwise we define the same thing again and again. (i.e. split it to base address and subpath or something similar). The best is not to make assumptions where the server is being deployed, but to make this configurable.

Any ideas how this could be made possible? We are rarely hosting hapi server in the root. All works fine if you only use /fhir endpoints, but we are loosing nice build in UI that comes with hapi-server unless we do workarounds: image

jvitrifork commented 3 years ago

The docker file is an example of what you can do. If you prefer not to have it hosted at ROOT, then rename the war to eg. 'myfancyurl.war'. If you deploy myfancyurl.war to tomcat, the content will be hosted under https://host.docker.internal/myfancyurl (UI) and https://host.docker.internal/myfancyurl/fhir (the actual FHIR endpoint). Remember to reconfigure the tester as well in that case

jvitrifork commented 3 years ago

eg. https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/application.yaml#L93 where it then should say https://host.docker.internal/myfancyurl/fhir

petermicuch commented 3 years ago

@jvitrifork thanks for your response. I get what you are saying. I just wanted to somehow bring together all different settings that are in hapi with regards to subpath/base url. I.e. this should also be reflected to tester server settings. But that might be specific to my usecase.

In my case, i am using one hapi docker instance for one fhir version as seen also on this output from my local cluster.

NAME                                           READY   STATUS    RESTARTS   AGE
hapi-postgres-0                                1/1     Running   2          40h
hapi-server-dstu2-6869569d9b-sbq8n             1/1     Running   2          40h
hapi-server-dstu3-7d4996994-8xkrx              1/1     Running   1          23h
hapi-server-r4-6d5ff58477-cdsf9                1/1     Running   2          40h

So there is just one tester defined per instance and you can reach different versions on different subpaths i.e. https://docker.host.internal/fhir-dstu2 (+/fhir for tester), https://docker.host.internal/fhir-dstu3 (+/fhir for tester) and https://docker.host.internal/fhir-r4 (+/fhir for tester). Since I am able to achieve this by modifying original docker file, I am closing this issue. Thank you.

petermicuch commented 3 years ago

After all I have to reopen this to at least get your opinion @jvitrifork and @jamesagnew. I am not sure if this is for starter or for base hapi project. When I try to run hapi server outside of ROOT with the modification to docker file as proposed also by @jvitrifork, then there are some hardcoded relative paths that will return 404. All works fine if you end your requested URL with /. I think this would deserve a fix not to force everyone to enforce redirects in the api gateway.

How to reproduce: Try to change Dockerfile#L13 so that application is reachable at "fhir-dstu3":

COPY --from=build-hapi /tmp/hapi-fhir-jpaserver-starter/target/*.war /usr/local/tomcat/webapps/fhir-dstu3.war

Then run this docker command (replace image name at the end of course):

docker run -p 8088:8080  -e hapi_fhir_fhir_version=DSTU3 -e hapi_fhir_server_address="http://localhost:8088/fhir-dstu3/fhir" name_of_your_docker_image

Then open browser and disable cache so that you do not get things loaded from cache and navigate to http://localhost:8088/fhir-dstu3

You will see that some resources are not loaded, like e.g. custom logo and several scripts. If you call http://localhost:8088/fhir-dstu3/ all works fine, since last / treats path as directory and not file.

Milzor commented 3 years ago

+1

I want to use the same dockerfile for multiple instance on the same host. I have an nginx ingress in front of the hapi fhir instances which forwards the traffic based on path to the correct k8s service.

fhir.host.com/r4/fhir fhir.host.com/stu3/fhir

The hapi fhir server tries to load the resources at root level which doesn't resolve.

fhir.host.com/resources/Eonasdan-bootstrap-datetimepicker/js/bootstrap-datetimepicker.js

Should be some kind of property to configure the location of the resources.

jamesagnew commented 3 years ago

FWIW the paths come from here: https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/tmpl-head.html#L33

Specifically, using the thymeleaf @{...path....} syntax should cause the context to be added automatically, provided your server actually knows what context it's running under

Milzor commented 3 years ago

@jamesagnew tnx for your reply. How to actually set this context?

petermicuch commented 3 years ago

@Milzor if you do the same that I did, it will work just fine with very few leftover 404 responses, which are obviously hardcoded and ignoring "base_href" settings - and that is why I reopened this issue.

Find below an example of the modified Dockerfile. Keep everything the same, only change the last part of runtime image, where you need to introduce environment variable (in my case BASE_URI) and use that one to rename the *.war file to you specific subpath.

BTW - @jamesagnew could I provide PR for this change or is there more clever way to do this?

#Keep the original Dockerfile content up to tomcat:9.0.38-jdk11-openjdk-slim-buster image
FROM tomcat:9.0.38-jdk11-openjdk-slim-buster

RUN mkdir -p /data/hapi/lucenefiles && chmod 775 /data/hapi/lucenefiles
COPY --from=build-hapi /tmp/hapi-fhir-jpaserver-starter/target/*.war /usr/local/tomcat/webapps/ROOT.tmp

ENV BASE_URI "/ROOT"

EXPOSE 8080

CMD mv /usr/local/tomcat/webapps/ROOT.tmp /usr/local/tomcat/webapps${BASE_URI}.war && exec catalina.sh run
jkiddo commented 3 years ago

@petermicuch If you feel the ENV-controlled filename feature adds value, then please make a PR ;)

Milzor commented 3 years ago

So, I took a different approach and now use a spring-boot war with embedded tomcat to run the application in a docker container. This way you can use the following command to start the container.

java -jar /path/to/war.war

passing an env variable SERVER_SERVLET_CONTEXT_PATH will make spring initiate the application context on the configured path.

petermicuch commented 3 years ago

@Milzor where do you copy war file during the docker build? Does this -jar refer always to root.war file and only specifies different context path or does the file need to be located at that path?

Milzor commented 3 years ago

I use a multi stage docker build in my CI.

....stuff before
RUN ./mvnw clean package -Pboot
COPY --from=builder target/ROOT.war /
CMD ["java", "-jar", "/ROOT.war"]

So the name of the war doesn't need to change. Spring boot uses application.yml to load the properties. I even integrated spring-cloud-config-server to provide configuration for each individual fhir server.

If you don't add -Pboot to your mvn command it won't add the embedded tomcat and then you need an external web server to run the war.

rahatrafiq1810 commented 2 years ago

I use a multi stage docker build in my CI.

....stuff before
RUN ./mvnw clean package -Pboot
COPY --from=builder target/ROOT.war /
CMD ["java", "-jar", "/ROOT.war"]

So the name of the war doesn't need to change. Spring boot uses application.yml to load the properties. I even integrated spring-cloud-config-server to provide configuration for each individual fhir server.

If you don't add -Pboot to your mvn command it won't add the embedded tomcat and then you need an external web server to run the war.

@Milzor Could you please share your entire Dockerfile? They have changed the dockerfile significantly over the last year and I can't successfully reproduce your solution in the current context.

Milzor commented 2 years ago

Well the idea is still the same. You can replace the entire dockerfile of this project for your own.

It's quite different in my case now as I removed the boot maven profile from the pom and rely fully on spring boot locally and in docker. Steps to accomplish this:

  1. Remove boot profile from the pom
  2. Add spring dependency bom and any spring boot dependency you want
    <properties>
        ....
        <spring-boot.version>2.6.7</spring-boot.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        ....
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
  3. Replace Dockerfile with
    
    FROM adoptopenjdk/openjdk11:jre-11.0.14.1_1-alpine

RUN mkdir -p /data/hapi/lucenefiles && \ chmod 775 /data/hapi/lucenefiles

COPY target/ROOT.war /app/

EXPOSE 8080

CMD ["java", "-jar", "/app/ROOT.war"]


4. run `mvn clean package`
5. run `docker build -t hapi-fhir-jpaserver-starter .`
6. run `docker run -it --rm -e SERVER_SERVLET_CONTEXT_PATH=/pathyoulike -e SERVER_PORT=8080 hapi-fhir-jpaserver-starter`

feel free to use `mvn clean package` in the same dockerfile with a multi stage build or use a CI/CD to build the artifact.

let me know if this works out for you.
rahatrafiq1810 commented 2 years ago

@Milzor I have created the following multi stage Docker build. I have also added the depedencies you mentioned in pom.xml. However, I keep getting the following error when trying to run the application.

FROM maven:3.8-openjdk-17-slim as builder

RUN mkdir -p /app/source
COPY . /app/source
WORKDIR /app/source
RUN mvn clean package

FROM adoptopenjdk/openjdk11:jre-11.0.14.1_1-alpine as runtime

RUN mkdir -p /data/hapi/lucenefiles && \
    chmod 775 /data/hapi/lucenefiles
COPY --from=builder /app/source/target/ROOT.war /app/
EXPOSE 8080
CMD ["java", "-jar", "/app/ROOT.war"]
***************************
APPLICATION FAILED TO START
***************************

Description:

An attempt was made to call a method that does not exist. The attempt was made from the following location:

    org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:448)

The following method did not exist:

    'boolean org.springframework.util.ClassUtils.isLambdaClass(java.lang.Class)'

The calling method's class, org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator, was loaded from the following location:

    jar:file:/app/ROOT.war!/WEB-INF/lib/spring-aop-5.3.19.jar!/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.class

The called method's class, org.springframework.util.ClassUtils, is available from the following locations:

    jar:file:/app/ROOT.war!/WEB-INF/lib/spring-core-5.3.15.jar!/org/springframework/util/ClassUtils.class

The called method's class hierarchy was loaded from the following locations:

    org.springframework.util.ClassUtils: jar:file:/app/ROOT.war!/WEB-INF/lib/spring-core-5.3.15.jar!/

Action:

Correct the classpath of your application so that it contains compatible versions of the classes org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator and org.springfram
ework.util.ClassUtils

I think I have messed up somewhere in the dependencies section. Could you please share your pom file?

Milzor commented 2 years ago

Looks like you have an issue with spring dependencies. Try to run

mvn dependency:tree

and investigate what's wrong.

navin-rai commented 1 year ago

Facing same issue when deploying HAPI with Helm and using Ingress for access. anybody found solution ?

jkiddo commented 6 months ago

@petermicuch have you tried https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/106af029a25f8ba082aabf3c599d9128c5e90db4/src/main/resources/application.yaml#L4 ?