1
1
mirror of https://github.com/MarginaliaSearch/MarginaliaSearch.git synced 2025-10-06 17:32:39 +02:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Viktor Lofgren
5ee4321110 (ci) Correct deploy script 2024-12-22 20:08:37 +01:00
Viktor Lofgren
9459b9933b (ci) Correct deploy script 2024-12-22 19:40:32 +01:00
Viktor Lofgren
87fb564f89 (ci) Add script for automatic deployment based on git tags 2024-12-22 19:24:54 +01:00
Viktor Lofgren
5ca8523220 (math) Reduce log error spam from null unit conversions 2024-12-21 18:51:45 +01:00
Viktor Lofgren
1118657ffd (system) Supply local IP to service discovery if multiFace is enabled 2024-12-19 22:20:19 +01:00
Viktor Lofgren
b1f970152d (system) To support configurations with multiple docker networks, bind to the "most local" interface.
Make the behavior optional.
2024-12-19 20:26:31 +01:00
Viktor Lofgren
e1783891ab (system) To support configurations with multiple docker networks, bind to the "most local" interface. 2024-12-19 20:18:57 +01:00
Viktor Lofgren
64d32471dd (deploy) Deploy executor test 2024-12-19 17:45:47 +01:00
6 changed files with 331 additions and 7 deletions

View File

@@ -6,6 +6,9 @@ import nu.marginalia.service.ServiceId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.Objects;
import java.util.UUID;
@@ -69,6 +72,17 @@ public class ServiceConfigurationModule extends AbstractModule {
return configuredValue;
}
if (Boolean.getBoolean("system.multiFace")) {
try {
String localNetworkIp = getLocalNetworkIP();
if (null != localNetworkIp) {
return localNetworkIp;
}
}
catch (Exception ex) {
logger.warn("Failed to get local network IP", ex);
}
}
// If we're in docker, we'll use the hostname
if (Boolean.getBoolean("service.useDockerHostname")) {
return System.getenv("HOSTNAME");
@@ -84,10 +98,41 @@ public class ServiceConfigurationModule extends AbstractModule {
private String getBindAddress() {
String configuredValue = System.getProperty("service.bind-address");
if (configuredValue != null) {
logger.info("Using configured bind address {}", configuredValue);
return configuredValue;
}
return "127.0.0.1";
if (Boolean.getBoolean("system.multiFace")) {
try {
return Objects.requireNonNullElse(getLocalNetworkIP(), "0.0.0.0");
} catch (Exception ex) {
logger.warn("Failed to get local network IP, falling back to bind to 0.0.0.0", ex);
return "0.0.0.0";
}
}
else {
return "0.0.0.0";
}
}
public static String getLocalNetworkIP() throws Exception {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
while (nets.hasMoreElements()) {
NetworkInterface netif = nets.nextElement();
if (!netif.isUp() || netif.isLoopback()) {
continue;
}
Enumeration<InetAddress> inetAddresses = netif.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress addr = inetAddresses.nextElement();
if (addr.isSiteLocalAddress() && !addr.isLoopbackAddress()) {
return addr.getHostAddress();
}
}
}
return null;
}
}

View File

@@ -7,6 +7,8 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import java.net.InetSocketAddress;
public class MetricsServer {
@Inject
@@ -15,7 +17,8 @@ public class MetricsServer {
if (configuration.metricsPort() < 0)
return;
Server server = new Server(configuration.metricsPort());
Server server = new Server(new InetSocketAddress(configuration.bindAddress(), configuration.metricsPort()));
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/");
server.setHandler(context);

View File

@@ -49,13 +49,14 @@ public class Units {
var fromUnit = unitsByName.get(fromUnitName.toLowerCase());
var toUnit = unitsByName.get(toUnitName.toLowerCase());
if (Objects.equals(fromUnit, toUnit)) {
return Optional.of(value + " " + fromUnit.name);
}
if (null == fromUnit || null == toUnit) {
return Optional.empty();
}
if (Objects.equals(fromUnit, toUnit)) {
return Optional.of(value + " " + fromUnit.name);
}
if (!Objects.equals(toUnit.type, fromUnit.type)) {
return Optional.empty();
}

View File

@@ -2,4 +2,5 @@
A master HEAD tagged with deploy-core*, deploy-executor*, or deploy-index* will trigger a commit.
2024-12-19: Test deployment of executor
2024-12-19-00002: Test deployment of executor
2024-12-19-00001: Test deployment of executor

View File

@@ -8,7 +8,7 @@ jib {
}
container {
mainClass = application.mainClass
jvmFlags = ['-Dservice.bind-address=0.0.0.0', '-Dservice.useDockerHostname=TRUE', '-Dsystem.homePath=/wmsa']
jvmFlags = ['-Dservice.useDockerHostname=TRUE', '-Dsystem.homePath=/wmsa']
volumes = ['/wmsa/conf', '/wmsa/model', '/wmsa/data', '/var/log/wmsa']
}
}

View File

@@ -0,0 +1,274 @@
from dataclasses import dataclass
import subprocess, os
from typing import List, Set, Dict, Optional
build_dir = "/app/search.marginalia.nu/build"
docker_dir = "/app/search.marginalia.nu/docker"
@dataclass
class ServiceConfig:
"""Configuration for a service"""
gradle_target: str
docker_name: str
instances: int | None
deploy_tier: int
@dataclass
class DeploymentPlan:
services_to_build: List[str]
instances_to_hold: Set[str]
@dataclass
class DockerContainer:
name: str
partition: int
config: ServiceConfig
def docker_name(self) -> str:
if self.partition < 1:
return f"{self.name}"
return f"{self.name}-{self.partition}"
def deploy_key(self) -> str:
return f"{self.config.deploy_tier}.{self.partition}"
class BuildError(Exception):
"""Raised when a build fails"""
def __init__(self, service: str, return_code: int):
self.service = service
self.return_code = return_code
super().__init__(f"Build failed for {service} with code {return_code}")
def get_deployment_tag() -> str | None:
"""Get the deployment tag from the current HEAD commit, if one exists."""
cmd = ['git', 'for-each-ref', '--points-at', 'HEAD', 'refs/tags', '--format=%(refname:short) %(subject)']
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"Git command failed: {result.stderr}")
for tag in result.stdout.splitlines():
if tag.startswith('deploy-'):
return tag.split(' ')[1:]
return None
def parse_deployment_tags(
tag_messages: List[str],
service_config: Dict[str, ServiceConfig]
) -> DeploymentPlan:
"""
Parse deployment and hold tags using service configuration.
Args:
tag_messages: List of tag messages (e.g. ['deploy:all,-frontend', 'hold:index-service-7'])
service_config: Dictionary mapping service names to their configuration
Returns:
DeploymentPlan containing services to build and instances to hold
"""
services_to_build = set()
services_to_exclude = set()
instances_to_hold = set()
available_services = set(service_config.keys())
for tag in [tag.strip() for tag in tag_messages]:
if tag.startswith('deploy:'):
parts = tag[7:].strip().split(',')
for part in parts:
part = part.strip()
if part == 'all':
services_to_build.update(available_services)
elif part.startswith('-'):
services_to_exclude.add(part[1:])
elif part.startswith('+'):
services_to_build.add(part[1:])
elif tag.startswith('hold:'):
instances = tag[5:].strip().split(',')
instances_to_hold.update(i.strip() for i in instances if i.strip())
# Remove any explicitly excluded services
services_to_build = services_to_build - services_to_exclude
# Validate that all specified services exist
invalid_services = (services_to_build | services_to_exclude) - available_services
if invalid_services:
raise ValueError(f"Unknown services specified: {invalid_services}")
return DeploymentPlan(
services_to_build=sorted(list(services_to_build)),
instances_to_hold=instances_to_hold
)
def deploy_container(container: DockerContainer) -> None:
"""
Run a docker deployment for the specified service and target.
Raises BuildError if the build fails.
"""
print(f"Deploying {container.name}")
process = subprocess.Popen(
['docker', 'compose', '--progress', 'quiet', 'up', '-d', container.name],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
# Stream output in real-time
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.rstrip())
return_code = process.poll()
if return_code != 0:
raise BuildError(container, return_code)
def deploy_services(containers: List[str]) -> None:
cwd = os.getcwd()
os.chdir(docker_dir)
for container in containers:
deploy_container(container)
def build_and_deploy(plan: DeploymentPlan, service_config: Dict[str, ServiceConfig]):
"""Execute the deployment plan"""
for service in plan.services_to_build:
config = service_config[service]
print(f"Building {service}:")
run_gradle_build(service, config.gradle_target)
to_deploy = list()
for service in plan.services_to_build:
config = service_config[service]
if config.instances == None:
if config.docker_name in plan.instances_to_hold:
continue
container = DockerContainer(config.docker_name, 0, config)
to_deploy.append(container)
else:
for instance in range(1,config.instances + 1):
if config.docker_name in plan.instances_to_hold:
continue
container_name = f"{config.docker_name}-{instance}"
if container_name in plan.instances_to_hold:
continue
to_deploy.append(DockerContainer(container_name, instance, config))
to_deploy = sorted(to_deploy, key = lambda c : c.deploy_key())
deploy_services(to_deploy)
def run_gradle_build(service: str, target: str) -> None:
"""
Run a Gradle build for the specified service and target.
Raises BuildError if the build fails.
"""
print(f"\nBuilding {service} with target {target}")
process = subprocess.Popen(
['./gradlew', '-q', target],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True
)
# Stream output in real-time
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.rstrip())
return_code = process.poll()
if return_code != 0:
raise BuildError(service, return_code)
# Example usage:
if __name__ == '__main__':
# Define service configuration
SERVICE_CONFIG = {
'search': ServiceConfig(
gradle_target=':code:services-application:search-service:docker',
docker_name='search-service',
instances=2,
deploy_tier=2
),
'api': ServiceConfig(
gradle_target=':code:services-application:api-service:docker',
docker_name='api-service',
instances=2,
deploy_tier=1
),
'api': ServiceConfig(
gradle_target=':code:services-core:assistant-service:docker',
docker_name='assistant-service',
instances=2,
deploy_tier=2
),
'explorer': ServiceConfig(
gradle_target=':code:services-application:explorer-service:docker',
docker_name='explorer-service',
instances=None,
deploy_tier=1
),
'dating': ServiceConfig(
gradle_target=':code:services-application:dating-service:docker',
docker_name='dating-service',
instances=None,
deploy_tier=1
),
'index': ServiceConfig(
gradle_target=':code:services-core:index-service:docker',
docker_name='index-service',
instances=10,
deploy_tier=3
),
'executor': ServiceConfig(
gradle_target=':code:services-core:executor-service:docker',
docker_name='executor-service',
instances=10,
deploy_tier=3
),
'control': ServiceConfig(
gradle_target=':code:services-core:control-service:docker',
docker_name='control-service',
instances=None,
deploy_tier=0
),
'query': ServiceConfig(
gradle_target=':code:services-core:query-service:docker',
docker_name='query-service',
instances=2,
deploy_tier=2
),
}
try:
tags = get_deployment_tag()
if tags != None:
print("Found deployment tags:", tags)
plan = parse_deployment_tags(tags, SERVICE_CONFIG)
print("\nDeployment Plan:")
print("Services to build:", plan.services_to_build)
print("Instances to hold:", plan.instances_to_hold)
print("\nExecution Plan:")
build_and_deploy(plan, SERVICE_CONFIG)
else:
print("No tags found")
except ValueError as e:
print(f"Error: {e}")