Skip to content

Commit

Permalink
feat: initial app controller code (#2)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladislav Yatsun <[email protected]>
  • Loading branch information
Oleksii-Klimov and nepalevov authored Nov 29, 2024
1 parent fe994e2 commit 8891a35
Show file tree
Hide file tree
Showing 67 changed files with 2,895 additions and 8 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/dependency-reivew.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Dependency Review

on:
pull_request_target:
types:
- opened
- synchronize

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
dependency-review:
uses: epam/ai-dial-ci/.github/workflows/[email protected]
secrets:
ACTIONS_BOT_TOKEN: ${{ secrets.ACTIONS_BOT_TOKEN }}
6 changes: 5 additions & 1 deletion .github/workflows/pr-title-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ on:
types:
- opened
- edited
- synchronize
- reopened

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
pr-title-check:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ jobs:
run_tests:
uses: epam/ai-dial-ci/.github/workflows/[email protected]
secrets: inherit
with:
java_version: 21
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ on:
push:
branches: [development, release-*]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
release:
uses: epam/ai-dial-ci/.github/workflows/[email protected]
secrets: inherit
with:
java_version: 21
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
FROM gradle:8.10-jdk17-alpine AS cache
FROM gradle:8.10-jdk21-alpine AS cache

WORKDIR /home/gradle/src
ENV GRADLE_USER_HOME="/cache"
COPY build.gradle settings.gradle ./
# just pull dependencies for cache
RUN gradle --no-daemon build --stacktrace

FROM gradle:8.10-jdk17-alpine AS builder
FROM gradle:8.10-jdk21-alpine AS builder

COPY --from=cache /cache /home/gradle/.gradle
COPY --chown=gradle:gradle . /home/gradle/src

WORKDIR /home/gradle/src
RUN gradle --no-daemon build --stacktrace -PdisableCompression=true -x test

RUN mkdir /build && tar -xf /home/gradle/src/build/distributions/__RENAMEMEPLEASE__*.tar --strip-components=1 -C /build
RUN mkdir /build && tar -xf /home/gradle/src/build/distributions/ai-dial-app-controller*.tar --strip-components=1 -C /build

FROM eclipse-temurin:17-jdk-alpine
FROM eclipse-temurin:21-jdk-alpine

ENV OTEL_TRACES_EXPORTER="none"
ENV OTEL_METRICS_EXPORTER="none"
Expand All @@ -36,4 +36,4 @@ HEALTHCHECK --start-period=30s --interval=1m --timeout=3s \

EXPOSE 8080 9464

ENTRYPOINT ["/app/bin/__RENAMEMEPLEASE__"]
ENTRYPOINT ["/app/bin/ai-dial-app-controller"]
31 changes: 30 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,43 @@ apply plugin: 'io.freefair.lombok'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
languageVersion = JavaLanguageVersion.of(21)
}
}

checkstyle {
configDirectory = file("$rootProject.projectDir/checkstyle")
configProperties = [
"org.checkstyle.google.suppressionfilter.config":
"$rootProject.projectDir/checkstyle/checkstyle-suppressions.xml",
]
}

repositories {
mavenCentral()
}

dependencies {
implementation 'io.kubernetes:client-java:22.0.0'
implementation 'commons-io:commons-io:2.16.1'
implementation 'org.springframework.boot:spring-boot-starter-web:3.4.0'
implementation 'org.springframework.boot:spring-boot-starter-webflux:3.4.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'

testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.4.0'
testImplementation 'io.projectreactor:reactor-test:3.6.10'
testImplementation 'org.assertj:assertj-core:3.26.3'

// Security fix:
implementation 'io.netty:netty-common:4.1.115.Final' // CVE-2024-47535
}

application {
mainClass = 'com.epam.aidial.Application'
}

test {
useJUnitPlatform()
}
1 change: 1 addition & 0 deletions builder/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dockerfile
26 changes: 26 additions & 0 deletions builder/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM python:3.11-alpine

ENV DIAL_BASE_URL=''
ENV SOURCES=''
ENV TARGET_DIR=/sources
ENV TEMPLATES_DIR=/templates
ENV API_KEY=''
ENV JWT=''

RUN pip install --no-cache-dir requests==2.32.3 packaging==24.1

WORKDIR /builder

RUN adduser -u 1001 --disabled-password --gecos "" appuser

# Prepare "output" directories for code sources and docker image templates
RUN mkdir ${TARGET_DIR} ${TEMPLATES_DIR} && \
chown -R appuser:appuser ${TARGET_DIR} ${TEMPLATES_DIR}

COPY . .

RUN chmod +x entrypoint.sh

USER appuser

ENTRYPOINT ["/builder/entrypoint.sh"]
118 changes: 118 additions & 0 deletions builder/download_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import os
from pathlib import Path
from typing import Mapping
from urllib.parse import urljoin, unquote

import requests
from packaging.requirements import Requirement


class AppValidationException(Exception):
pass


def download_files(
dial_base_url: str,
headers: Mapping,
source_base: str | None,
target: Path,
files_metadata: list[dict[str, any]],
):
for file_metadata in files_metadata:
if file_metadata["resourceType"] == "FILE":
url = file_metadata["url"]
file_path = target / unquote(url).removeprefix(source_base)
download_file(dial_base_url, headers, url, file_path)


def download_file(dial_base_url: str, headers: Mapping, file_url: str, target: Path):
file_url = urljoin(dial_base_url, "v1/" + file_url)
with requests.get(file_url, headers=headers, stream=True) as response:
response.raise_for_status()

print(f"{file_url} => {target}")
target.parent.mkdir(parents=True, exist_ok=True)

with target.open('wb') as file:
for chunk in response.iter_content(chunk_size=8192):
file.write(chunk)


def validate_entrypoint(target: Path):
entrypoint = target / "app.py"
if not entrypoint.exists():
raise AppValidationException("Missing entrypoint file: app.py")


def validate_packages(target: Path, allowed_packages: set[str]):
requirements = target / "requirements.txt"
requirements.touch(exist_ok=True)

with open(target / "requirements.txt") as lines:
for line in lines:
line = line.strip()
if not line or line.startswith('#'):
continue

requirement = parse_requirement(line)
package_name = requirement.name
if requirement.url:
raise AppValidationException(f"URLs are not allowed in requirements.txt: {requirement.url}")

if package_name not in allowed_packages:
raise AppValidationException(f"Package '{package_name}' is forbidden.")


def parse_requirement(line):
if line.startswith("-"):
raise AppValidationException(f"Pip options aren't allowed in requirements.txt: {line}")

try:
return Requirement(line)
except Exception as e:
raise AppValidationException(f"Unsupported requirement: {line}") from e


def main():
dial_base_url = os.environ["DIAL_BASE_URL"]
sources = os.environ["SOURCES"]
target = Path(os.environ["TARGET_DIR"])
api_key = os.getenv("API_KEY")
jwt = os.getenv("JWT")
allowed_packages = os.getenv("ALLOWED_PACKAGES")

print(f"Dial base url: {dial_base_url}")
print(f"Sources: {sources}")
print(f"Target folder: {target}")

headers: dict[str, str] = {}
if api_key:
headers["api-key"] = api_key
if jwt:
headers["Authorization"] = f"Bearer {jwt}"

metadata_url = urljoin(dial_base_url, f"v1/metadata/{sources}")
params: dict[str, str] = {"recursive": "true"}
while True:
with requests.get(metadata_url, params, headers=headers) as response:
response.raise_for_status()

result: dict[str, any] = response.json()

if not result["nodeType"] == "FOLDER":
raise AppValidationException("Sources path must be a folder")

download_files(dial_base_url, headers, unquote(sources), target, result.get("items", []))

token = result.get("nextToken")
if not token:
break

params["token"] = token

validate_entrypoint(target)
validate_packages(target, set(allowed_packages.split()))


if __name__ == "__main__":
main()
7 changes: 7 additions & 0 deletions builder/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/sh

set -e

cp -r templates/* "$TEMPLATES_DIR"

exec python download_files.py
1 change: 1 addition & 0 deletions builder/templates/python-pip/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dockerfile
23 changes: 23 additions & 0 deletions builder/templates/python-pip/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
ARG PYTHON_IMAGE
FROM $PYTHON_IMAGE

# Allow statements and log messages to immediately appear in the logs
ENV PYTHONUNBUFFERED=True

# Create a non-root user
RUN adduser -u 1001 --disabled-password --gecos "" appuser

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir uvicorn==0.31.0 \
&& pip install --no-cache-dir --requirement requirements.txt

COPY --chown=appuser . .

EXPOSE 8080

USER appuser

ENTRYPOINT ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
10 changes: 10 additions & 0 deletions checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0"?>

<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">

<suppressions>
<!-- Suppress all checks for 3rd party knative classes -->
<suppress checks=".*" files="com[\\/]epam[\\/]aidial[\\/]kubernetes[\\/]knative[\\/]" />
</suppressions>
Empty file modified gradlew
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1 +1 @@
rootProject.name = '__RENAMEMEPLEASE__'
rootProject.name = 'ai-dial-app-controller'
11 changes: 11 additions & 0 deletions src/main/java/com/epam/aidial/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.epam.aidial;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Loading

0 comments on commit 8891a35

Please sign in to comment.