diff --git a/.github/workflows/geonames-image.yaml b/.github/workflows/geonames-image.yaml new file mode 100644 index 0000000000..5444b633c4 --- /dev/null +++ b/.github/workflows/geonames-image.yaml @@ -0,0 +1,87 @@ +name: geonames-image +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + inputs: + build_arm: + type: boolean + description: "Build for ARM as well" + default: false + required: false + workflow_call: + inputs: + build_arm: + type: boolean + description: "Build for ARM as well" + default: false + required: false +env: + DOCKER_IMAGE_NAME: ghcr.io/loculus-project/geonames + BRANCH_NAME: ${{ github.head_ref || github.ref_name }} + BUILD_ARM: ${{ github.event.inputs.build_arm || inputs.build_arm || github.ref == 'refs/heads/main' }} + sha: ${{ github.event.pull_request.head.sha || github.sha }} +concurrency: + group: ci-${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }}-geonames-${{github.event.inputs.build_arm}} + cancel-in-progress: true +jobs: + geonames-image: + name: Build geonames Docker Image # Don't change: Referenced by .github/workflows/update-argocd-metadata.yml + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + packages: write + checks: read + steps: + - name: Shorten sha + run: echo "sha=${sha::7}" >> $GITHUB_ENV + - uses: actions/checkout@v4 + - name: Generate files hash + id: files-hash + run: | + DIR_HASH=$(echo -n ${{ hashFiles('geonames/**', '.github/workflows/geonames-image.yml') }}) + echo "DIR_HASH=$DIR_HASH${{ env.BUILD_ARM == 'true' && '-arm' || '' }}" >> $GITHUB_ENV + - name: Setup Docker metadata + id: dockerMetadata + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKER_IMAGE_NAME }} + tags: | + type=raw,value=${{ env.DIR_HASH }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=${{ env.BRANCH_NAME }} + type=raw,value=commit-${{ env.sha }} + type=raw,value=${{ env.BRANCH_NAME }}-arm,enable=${{ env.BUILD_ARM }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Check if image exists + id: check-image + run: | + EXISTS=$(docker manifest inspect ${{ env.DOCKER_IMAGE_NAME }}:${{ env.DIR_HASH }} > /dev/null 2>&1 && echo "true" || echo "false") + echo "CACHE_HIT=$EXISTS" >> $GITHUB_ENV + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push image if input files changed + if: env.CACHE_HIT == 'false' + uses: docker/build-push-action@v6 + with: + context: ./geonames + push: true + tags: ${{ steps.dockerMetadata.outputs.tags }} + cache-from: type=gha,scope=geonames-${{ github.ref }} + cache-to: type=gha,mode=max,scope=geonames-${{ github.ref }} + platforms: ${{ env.BUILD_ARM == 'true' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} + - name: Retag and push existing image if cache hit + if: env.CACHE_HIT == 'true' + run: | + TAGS=(${{ steps.dockerMetadata.outputs.tags }}) + for TAG in "${TAGS[@]}"; do + docker buildx imagetools create --tag $TAG ${{ env.DOCKER_IMAGE_NAME }}:${{ env.DIR_HASH }} + done diff --git a/geonames/.gitignore b/geonames/.gitignore new file mode 100644 index 0000000000..f26f047aea --- /dev/null +++ b/geonames/.gitignore @@ -0,0 +1,3 @@ +results/ +uploads/ +*.db \ No newline at end of file diff --git a/geonames/Dockerfile b/geonames/Dockerfile new file mode 100644 index 0000000000..d3f3c85515 --- /dev/null +++ b/geonames/Dockerfile @@ -0,0 +1,23 @@ +FROM mambaorg/micromamba:1.5.8 + +COPY --chown=$MAMBA_USER:$MAMBA_USER environment.yaml /tmp/env.yaml + +RUN micromamba config set extract_threads 1 \ + && micromamba install -y -n base -f /tmp/env.yaml \ + && micromamba clean --all --yes + +# Set the environment variable to activate the conda environment +ARG MAMBA_DOCKERFILE_ACTIVATE=1 + +COPY --chown=$MAMBA_USER:$MAMBA_USER . /package + +RUN mkdir -p /package/uploads +RUN mkdir -p /package/results + +WORKDIR /package +ENV PATH="/opt/conda/bin:$PATH" + +EXPOSE 5000 + +ENTRYPOINT ["/bin/bash", "-c"] +CMD ["sh"] \ No newline at end of file diff --git a/geonames/README.md b/geonames/README.md new file mode 100644 index 0000000000..62d761d070 --- /dev/null +++ b/geonames/README.md @@ -0,0 +1,31 @@ +## Geonames API + +This is a simple flask API with a local SQLlite database server and swagger API, the API can be run locally using + +``` +python api.py +``` + +initially the database will be empty but Geonames offers a free download service with all administrative regions: https://download.geonames.org/export/dump/, see local development for details. + +### Local Development + +[Dbeaver](https://dbeaver.io/) is great interface for SQLlite - enter the path to `geonames_database.db` to view the local database. + +Run the following commands to download all administrative regions from Geonames and upload to the SQLlite db. + +``` +wget https://download.geonames.org/export/dump/allCountries.zip -O results/allCountries.zip +unzip results/allCountries.zip +tsv-filter --str-eq 7:A results/allCountries.txt > results/adm.tsv +tsv-select -f 1-3,5-6,8-13 results/adm.tsv > results/adm_dropped.tsv +curl -X POST -F "file=@results/adm_dropped.tsv" http://127.0.0.1:5000/upload/upload-tsv +``` + + +If you want to test the docker image locally. It can be built and run using the commands: + +```sh +docker build -t geonames . +docker run -p 5000:5000 geonames +``` \ No newline at end of file diff --git a/geonames/api.py b/geonames/api.py new file mode 100644 index 0000000000..53dd2c8dbd --- /dev/null +++ b/geonames/api.py @@ -0,0 +1,160 @@ +import csv +import os +import sqlite3 + +import yaml +from flask import Flask, request +from flask_restx import Api, Resource + +app = Flask(__name__) +api = Api(app, title="Geoname API", description="A simple API to manage geoname data") +DB_PATH = "geonames_database.db" +SCHEMA_PATH = "schema.sql" +UPLOAD_FOLDER = "uploads" + +# Ensure the upload folder exists +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER + +search = api.namespace("search", description="Geoname search operations") + + +# Initialize SQLite database from schema file +def init_db(): + with sqlite3.connect(DB_PATH) as conn: + with open(SCHEMA_PATH, "r") as schema_file: + conn.executescript(schema_file.read()) + file_path = os.path.normpath(os.path.join(app.config["UPLOAD_FOLDER"], "input.tsv")) + insert_tsv_to_db(file_path) + print("Database initialized successfully!") + +@search.route("/get-admin1") +class SearchAdmin1(Resource): + @api.doc(params={"query": "Get list of all admin1_codes for a given INSDC country"}) + def get(self): + """Get list of all admin1_codes for a given country_code""" + query = request.args.get("query", "") + if not query: + return {"error": "Query parameter is required"}, 400 + + country_code = app.config["insdc_country_code_mapping"].get(query, None) + + if not country_code: + return {"error": "Invalid country code"}, 400 + + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT asciiname + FROM administrative_regions + WHERE feature_code = 'ADM1' AND country_code = ?""", + (country_code,), + ) + results = cursor.fetchall() + return [row[0] for row in results] + except Exception as e: + return {"error": str(e)}, 500 + + +@search.route("/get-admin2") +class SearchAdmin2(Resource): + @api.doc(params={"query": "Get list of all admin1_codes for a given INSDC country"}) + def get(self): + """Get list of all admin1_codes for a given country_code""" + query = request.args.get("query", "") + if not query: + return {"error": "Query parameter is required"}, 400 + + country_code = app.config["insdc_country_code_mapping"].get(query, None) + + if not country_code: + return {"error": "Invalid country code"}, 400 + + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT asciiname + FROM administrative_regions + WHERE feature_code = 'ADM2' AND country_code = ?""", + (country_code,), + ) + results = cursor.fetchall() + return [row[0] for row in results] + except Exception as e: + return {"error": str(e)}, 500 + + +def insert_tsv_to_db(tsv_file_path): + try: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + # Open the TSV file for reading + with open(tsv_file_path, "r") as file: + tsv_reader = csv.reader(file, delimiter="\t") + + # Begin a transaction for bulk inserts + cursor.execute("BEGIN TRANSACTION;") + + # Loop through each row in the TSV and insert into the database + for row in tsv_reader: + # Adjust the SQL INSERT statement according to your table structure + cursor.execute( + """ + INSERT INTO administrative_regions + (geonameid, name, asciiname, latitude, longitude, feature_code, country_code, cc2, admin1_code, admin2_code, admin3_code) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + row, + ) # Pass the row as a tuple of values + + # Commit the transaction + cursor.execute("COMMIT;") + print("Data inserted successfully!") + + except Exception as e: + print(f"An error occurred: {e}") + return False + return True + + +upload = api.namespace("upload", description="Geoname upload operations") + + +# Define the endpoint to handle file uploads +@upload.route("/upload-tsv", methods=["POST"]) +class UploadTSV(Resource): + @api.doc(params={"file": "tsv file to upload"}) + def post(self): + if "file" not in request.files: + return {"error": "No file part"}, 400 + + file = request.files["file"] + + if not file.filename: + return {"error": "No selected file"}, 400 + + if file and file.filename.endswith(".tsv"): + # Save the file to the uploads directory + file_path = os.path.normpath(os.path.join(app.config["UPLOAD_FOLDER"], file.filename)) + if not file_path.startswith(app.config["UPLOAD_FOLDER"]): + return {"error": "Invalid file path."}, 400 + file.save(file_path) + + # Insert data from the TSV file into the database + if insert_tsv_to_db(file_path): + return {"message": "File successfully uploaded and data inserted."}, 200 + return {"error": "Failed to insert data into the database."}, 500 + else: + return {"error": "Invalid file format. Please upload a .tsv file."}, 400 + + +if __name__ == "__main__": + init_db() + config = yaml.safe_load(open("config/default.yaml", encoding="utf-8")) + app.config["insdc_country_code_mapping"] = config.get("insdc_country_code_mapping", {}) + debug_mode = os.getenv("FLASK_DEBUG", "False").lower() in ("true", "1", "t") + app.run(debug=debug_mode, host="0.0.0.0", port=5000) diff --git a/geonames/config/default.yaml b/geonames/config/default.yaml new file mode 100644 index 0000000000..15234dc2ed --- /dev/null +++ b/geonames/config/default.yaml @@ -0,0 +1,298 @@ +insdc_country_code_mapping: + Afghanistan: AF + Albania: AL + Algeria: DZ + American Samoa: AS + Andorra: AD + Angola: AO + Anguilla: AI + Antarctica: AQ + Antigua and Barbuda: AG + Arctic Ocean: null + Argentina: AR + Armenia: AM + Aruba: AW + Ashmore and Cartier Islands: null + Atlantic Ocean: null + Australia: AU + Austria: AT + Azerbaijan: AZ + Bahamas: BS + Bahrain: BH + Baltic Sea: null + Baker Island: null + Bangladesh: BD + Barbados: BB + Bassas da India: null + Belarus: BY + Belgium: BE + Belize: BZ + Benin: BJ + Bermuda: BM + Bhutan: BT + Bolivia: BO + Borneo: null + Bosnia and Herzegovina: BA + Botswana: BW + Bouvet Island: BV + Brazil: BR + British Virgin Islands: VG + Brunei: BN + Bulgaria: BG + Burkina Faso: BF + Burundi: BI + Cambodia: KH + Cameroon: CM + Canada: CA + Cape Verde: CV + Cayman Islands: KY + Central African Republic: CF + Chad: TD + Chile: CL + China: CN + Christmas Island: CX + Clipperton Island: null + Cocos Islands: CC + Colombia: CO + Comoros: KM + Cook Islands: CK + Coral Sea Islands: null + Costa Rica: CR + Cote d'Ivoire: CI + Croatia: HR + Cuba: CU + Curacao: CW + Cyprus: CY + Czechia: CZ + Democratic Republic of the Congo: CD + Denmark: DK + Djibouti: DJ + Dominica: DM + Dominican Republic: DO + Ecuador: EC + Egypt: EG + El Salvador: SV + Equatorial Guinea: GQ + Eritrea: ER + Estonia: EE + Eswatini: SZ + Ethiopia: ET + Europa Island: null + Falkland Islands (Islas Malvinas): FK + Faroe Islands: FO + Fiji: FJ + Finland: FI + France: FR + French Guiana: GF + French Polynesia: PF + French Southern and Antarctic Lands: TF + Gabon: GA + Gambia: GM + Gaza Strip: null + Georgia: GE + Germany: DE + Ghana: GH + Gibraltar: GI + Glorioso Islands: null + Greece: GR + Greenland: GL + Grenada: GD + Guadeloupe: GP + Guam: GU + Guatemala: GT + Guernsey: GG + Guinea: GN + Guinea-Bissau: GW + Guyana: GY + Haiti: HT + Heard Island and McDonald Islands: HM + Honduras: HN + Hong Kong: HK + Howland Island: null + Hungary: HU + Iceland: IS + India: IN + Indian Ocean: null + Indonesia: ID + Iran: IR + Iraq: IQ + Ireland: IE + Isle of Man: IM + Israel: IL + Italy: IT + Jamaica: JM + Jan Mayen: null + Japan: JP + Jarvis Island: null + Jersey: JE + Johnston Atoll: null + Jordan: JO + Juan de Nova Island: null + Kazakhstan: KZ + Kenya: KE + Kerguelen Archipelago: null + Kingman Reef: null + Kiribati: KI + Kosovo: XK + Kuwait: KW + Kyrgyzstan: KG + Laos: LA + Latvia: LV + Lebanon: LB + Lesotho: LS + Liberia: LR + Libya: LY + Liechtenstein: LI + Line Islands: null + Lithuania: LT + Luxembourg: LU + Macau: MO + Madagascar: MG + Malawi: MW + Malaysia: MY + Maldives: MV + Mali: ML + Malta: MT + Marshall Islands: MH + Martinique: MQ + Mauritania: MR + Mauritius: MU + Mayotte: YT + Mediterranean Sea: null + Mexico: MX + Micronesia, Federated States of: FM + Midway Islands: null + Moldova: MD + Monaco: MC + Mongolia: MN + Montenegro: ME + Montserrat: MS + Morocco: MA + Mozambique: MZ + Myanmar: MM + Namibia: NA + Nauru: NR + Navassa Island: null + Nepal: NP + Netherlands: NL + New Caledonia: NC + New Zealand: NZ + Nicaragua: NI + Niger: NE + Nigeria: NG + Niue: NU + Norfolk Island: NF + North Korea: KP + North Macedonia: MK + North Sea: null + Northern Mariana Islands: MP + Norway: NO + Oman: OM + Pacific Ocean: null + Pakistan: PK + Palau: PW + Palmyra Atoll: null + Panama: PA + Papua New Guinea: PG + Paracel Islands: null + Paraguay: PY + Peru: PE + Philippines: PH + Pitcairn Islands: PN + Poland: PL + Portugal: PT + Puerto Rico: PR + Qatar: QA + Republic of the Congo: CG + Reunion: RE + Romania: RO + Ross Sea: null + Russia: RU + Rwanda: RW + Saint Barthelemy: BL + Saint Helena: SH + Saint Kitts and Nevis: KN + Saint Lucia: LC + Saint Martin: MF + Saint Pierre and Miquelon: PM + Saint Vincent and the Grenadines: VC + Samoa: WS + San Marino: SM + Sao Tome and Principe: ST + Saudi Arabia: SA + Senegal: SN + Serbia: RS + Seychelles: SC + Sierra Leone: SL + Singapore: SG + Sint Maarten: SX + Slovakia: SK + Slovenia: SI + Solomon Islands: SB + Somalia: SO + South Africa: ZA + South Georgia and the South Sandwich Islands: GS + South Korea: KR + South Sudan: SS + Southern Ocean: null + Spain: ES + Spratly Islands: null + Sri Lanka: LK + State of Palestine: PS + Sudan: SD + Suriname: SR + Svalbard: SJ + Sweden: SE + Switzerland: CH + Syria: SY + Taiwan: TW + Tajikistan: TJ + Tanzania: TZ + Tasman Sea: null + Thailand: TH + Timor-Leste: TL + Togo: TG + Tokelau: TK + Tonga: TO + Trinidad and Tobago: TT + Tromelin Island: null + Tunisia: TN + Turkey: TR + Turkmenistan: TM + Turks and Caicos Islands: TC + Tuvalu: TV + Uganda: UG + Ukraine: UA + United Arab Emirates: AE + United Kingdom: GB + Uruguay: UY + USA: US + Uzbekistan: UZ + Vanuatu: VU + Venezuela: VE + Viet Nam: VN + Virgin Islands: VI + Wake Island: null + Wallis and Futuna: WF + West Bank: null + Western Sahara: EH + Yemen: YE + Zambia: ZM + Zimbabwe: ZW + Belgian Congo: CGO + British Guiana: BG + Burma: BU + Czechoslovakia: CS + Czech Republic: CZ + East Timor: TP + Korea: null + Macedonia: MK + Micronesia: FM + Netherlands Antilles: AN + Serbia and Montenegro: CS + Siam: null + Swaziland: SZ + The former Yugoslav Republic of Macedonia: MK + USSR: SU + Yugoslavia: YU + Zaire: ZR \ No newline at end of file diff --git a/geonames/environment.yaml b/geonames/environment.yaml new file mode 100644 index 0000000000..2e69791351 --- /dev/null +++ b/geonames/environment.yaml @@ -0,0 +1,18 @@ +name: loculus-geonames +channels: + - conda-forge + - bioconda +dependencies: + # Core Python dependencies + - python =3.12 + # Extra dependencies + - biopython + - flask + - psycopg2 + - PyYAML + - requests + - tsv-utils + - unzip + - curl + - flask-restx + - wget \ No newline at end of file diff --git a/geonames/import.sh b/geonames/import.sh new file mode 100644 index 0000000000..663ed51a8c --- /dev/null +++ b/geonames/import.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +wget https://download.geonames.org/export/dump/allCountries.zip -O results/allCountries.zip +unzip -o results/allCountries.zip -d results +rm results/allCountries.zip +tsv-filter --str-eq 7:A results/allCountries.txt > results/adm.tsv +sync && rm results/allCountries.txt +tsv-select -f 1-3,5-6,8-13 results/adm.tsv > uploads/input.tsv +rm results/adm.tsv \ No newline at end of file diff --git a/geonames/schema.sql b/geonames/schema.sql new file mode 100644 index 0000000000..e37e8182a4 --- /dev/null +++ b/geonames/schema.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS administrative_regions ( + geonameid INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(200) NOT NULL, + asciiname VARCHAR(200) NOT NULL, + latitude DECIMAL(9, 6) NOT NULL, + longitude DECIMAL(9, 6) NOT NULL, + feature_code VARCHAR(10), + country_code CHAR(2), + cc2 VARCHAR(200), + admin1_code VARCHAR(20), + admin2_code VARCHAR(80), + admin3_code VARCHAR(20) +); + +CREATE TABLE IF NOT EXISTS alternatenames ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + geonameid INTEGER REFERENCES geoname(geonameid) ON DELETE CASCADE, + alternatename VARCHAR(200) +); \ No newline at end of file diff --git a/kubernetes/loculus/templates/geonames-deployment.yaml b/kubernetes/loculus/templates/geonames-deployment.yaml new file mode 100644 index 0000000000..f4f3d325dc --- /dev/null +++ b/kubernetes/loculus/templates/geonames-deployment.yaml @@ -0,0 +1,44 @@ +{{- $dockerTag := include "loculus.dockerTag" .Values }} +{{- $keycloakTokenUrl := "http://loculus-keycloak-service:8083/realms/loculus/protocol/openid-connect/token" }} +{{- if not .Values.disableGeonames }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: loculus-geonames + annotations: + argocd.argoproj.io/sync-options: Replace=true + reloader.stakater.com/auto: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: loculus + component: geonames + template: + metadata: + labels: + app: loculus + component: geonames + spec: + {{- include "possiblePriorityClassName" $ | nindent 6 }} + initContainers: + - name: geonames-import + image: "ghcr.io/loculus-project/geonames:{{ $dockerTag }}" + command: ["sh", "./import.sh"] + resources: + limits: + cpu: 40m + memory: 4Gi + requests: + cpu: 40m + memory: 2Gi + containers: + - name: geonames-api + imagePullPolicy: IfNotPresent + image: "ghcr.io/loculus-project/geonames:{{ $dockerTag }}" + {{- include "loculus.resources" (list "geonames" $.Values) | nindent 10 }} + ports: + - containerPort: 5000 + command: ["python", "api.py"] +{{- end }} diff --git a/kubernetes/loculus/templates/geonames-service.yaml b/kubernetes/loculus/templates/geonames-service.yaml new file mode 100644 index 0000000000..c2bcc9d574 --- /dev/null +++ b/kubernetes/loculus/templates/geonames-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: loculus-geonames-service +spec: + selector: + app: loculus + component: geonames + ports: + - port: 5000 + targetPort: 5000 + protocol: TCP + name: http + type: ClusterIP \ No newline at end of file diff --git a/kubernetes/loculus/templates/ingressroute.yaml b/kubernetes/loculus/templates/ingressroute.yaml index 3c79a94261..3187085457 100644 --- a/kubernetes/loculus/templates/ingressroute.yaml +++ b/kubernetes/loculus/templates/ingressroute.yaml @@ -48,6 +48,7 @@ spec: {{- if eq $.Values.environment "server" }} {{- $backendHost := printf "backend%s%s" .Values.subdomainSeparator .Values.host }} {{- $keycloakHost := (printf "authentication%s%s" $.Values.subdomainSeparator $.Values.host) }} +{{- $geonamesHost := (printf "geonames%s%s" $.Values.subdomainSeparator $.Values.host) }} {{- $middlewareList := list (printf "%s-compression-middleware@kubernetescrd" $.Release.Namespace) }} {{- if $.Values.enforceHTTPS }} {{- $middlewareList = append $middlewareList (printf "%s-redirect-middleware@kubernetescrd" $.Release.Namespace) }} @@ -112,6 +113,28 @@ spec: --- apiVersion: networking.k8s.io/v1 kind: Ingress +metadata: + name: loculus-geonames-ingress + annotations: + traefik.ingress.kubernetes.io/router.middlewares: "{{ join "," $middlewareList }}" +spec: + rules: + - host: "{{ $geonamesHost }}" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: loculus-geonames-service + port: + number: 5000 + tls: + - hosts: + - "{{ $geonamesHost }}" +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress metadata: name: loculus-keycloak-ingress annotations: diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index b36cb49cc8..c850429fbb 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -10,6 +10,7 @@ disableBackend: false disablePreprocessing: false disableIngest: false disableEnaSubmission: true +disableGeonames: false website: websiteConfig: enableLoginNavigationItem: true