diff --git a/cli/dstack/_internal/backend/base/__init__.py b/cli/dstack/_internal/backend/base/__init__.py index 84db72556..b571bbe26 100644 --- a/cli/dstack/_internal/backend/base/__init__.py +++ b/cli/dstack/_internal/backend/base/__init__.py @@ -297,7 +297,13 @@ def list_jobs(self, repo_id: str, run_name: str) -> List[Job]: def run_job(self, job: Job, failed_to_start_job_new_status: JobStatus): self.predict_build_plan(job) # raises exception on missing build - base_jobs.run_job(self.storage(), self.compute(), job, failed_to_start_job_new_status) + base_jobs.run_job( + self.storage(), + self.compute(), + self.secrets_manager(), + job, + failed_to_start_job_new_status, + ) def restart_job(self, job: Job): base_jobs.restart_job(self.storage(), self.compute(), job) diff --git a/cli/dstack/_internal/backend/base/gateway.py b/cli/dstack/_internal/backend/base/gateway.py index f7a723d93..045529fe3 100644 --- a/cli/dstack/_internal/backend/base/gateway.py +++ b/cli/dstack/_internal/backend/base/gateway.py @@ -1,18 +1,21 @@ import subprocess -import time from typing import List, Optional +import pkg_resources + from dstack._internal.backend.base.compute import Compute from dstack._internal.backend.base.head import ( delete_head_object, list_head_objects, put_head_object, ) +from dstack._internal.backend.base.secrets import SecretsManager from dstack._internal.backend.base.storage import Storage -from dstack._internal.core.error import DstackError +from dstack._internal.core.error import SSHCommandError from dstack._internal.core.gateway import GatewayHead from dstack._internal.hub.utils.ssh import HUB_PRIVATE_KEY_PATH -from dstack._internal.utils.common import PathLike +from dstack._internal.utils.common import PathLike, removeprefix +from dstack._internal.utils.interpolator import VariablesInterpolator from dstack._internal.utils.random_names import generate_name @@ -37,34 +40,54 @@ def delete_gateway(compute: Compute, storage: Storage, instance_name: str): delete_head_object(storage, head) -def ssh_copy_id( +def resolve_hostname(secrets_manager: SecretsManager, repo_id: str, hostname: str) -> str: + secrets = {} + _, missed = VariablesInterpolator({}).interpolate(hostname, return_missing=True) + for ns_name in missed: + name = removeprefix(ns_name, "secrets.") + value = secrets_manager.get_secret(repo_id, name) + if value is not None: + secrets[name] = value.secret_value + return VariablesInterpolator({"secrets": secrets}).interpolate(hostname) + + +def publish( hostname: str, - public_key: bytes, + port: int, + ssh_key: bytes, user: str = "ubuntu", id_rsa: Optional[PathLike] = HUB_PRIVATE_KEY_PATH, -): - command = f"echo '{public_key.decode()}' >> ~/.ssh/authorized_keys" - exec_ssh_command(hostname, command, user=user, id_rsa=id_rsa) +) -> str: + command = ["sudo", "python3", "-", hostname, str(port), f'"{ssh_key.decode().strip()}"'] + with open( + pkg_resources.resource_filename("dstack._internal", "scripts/gateway_publish.py"), "r" + ) as f: + output = exec_ssh_command( + hostname, command=" ".join(command), user=user, id_rsa=id_rsa, stdin=f + ) + return output.decode().strip() -def exec_ssh_command(hostname: str, command: str, user: str, id_rsa: Optional[PathLike]) -> bytes: +def exec_ssh_command( + hostname: str, command: str, user: str, id_rsa: Optional[PathLike], stdin=None +) -> bytes: args = ["ssh"] if id_rsa is not None: args += ["-i", id_rsa] args += [ "-o", - "StrictHostKeyChecking=accept-new", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", f"{user}@{hostname}", command, ] - proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if not hostname: # ssh hangs indefinitely with empty hostname + raise SSHCommandError( + args, "ssh: Could not connect to the gateway, because hostname is empty" + ) + proc = subprocess.Popen(args, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = proc.communicate() if proc.returncode != 0: raise SSHCommandError(args, stderr.decode()) return stdout - - -class SSHCommandError(DstackError): - def __init__(self, cmd: List[str], message: str): - super().__init__(message) - self.cmd = cmd diff --git a/cli/dstack/_internal/backend/base/jobs.py b/cli/dstack/_internal/backend/base/jobs.py index 0ad0bdd8e..c5d34b40a 100644 --- a/cli/dstack/_internal/backend/base/jobs.py +++ b/cli/dstack/_internal/backend/base/jobs.py @@ -6,6 +6,7 @@ import dstack._internal.backend.base.gateway as gateway from dstack._internal.backend.base import runners from dstack._internal.backend.base.compute import Compute, InstanceNotFoundError, NoCapacityError +from dstack._internal.backend.base.secrets import SecretsManager from dstack._internal.backend.base.storage import Storage from dstack._internal.core.error import BackendError, BackendValueError, NoMatchingInstanceError from dstack._internal.core.instance import InstanceType @@ -119,6 +120,7 @@ def predict_job_instance( def run_job( storage: Storage, compute: Compute, + secrets_manager: SecretsManager, job: Job, failed_to_start_job_new_status: JobStatus, ): @@ -126,10 +128,16 @@ def run_job( raise BackendError("Can't create a request for a job which status is not SUBMITTED") try: if job.configuration_type == ConfigurationType.SERVICE: + job.gateway.hostname = gateway.resolve_hostname( + secrets_manager, job.repo_ref.repo_id, job.gateway.hostname + ) private_bytes, public_bytes = generate_rsa_key_pair_bytes(comment=job.run_name) - gateway.ssh_copy_id(job.gateway.hostname, public_bytes) + job.gateway.sock_path = gateway.publish( + job.gateway.hostname, job.gateway.public_port, public_bytes + ) job.gateway.ssh_key = private_bytes.decode() update_job(storage, job) + _try_run_job( storage=storage, compute=compute, @@ -163,7 +171,7 @@ def restart_job( def stop_job( - storage: Storage, compute: Compute, repo_id: str, job_id: str, terminate: str, abort: str + storage: Storage, compute: Compute, repo_id: str, job_id: str, terminate: bool, abort: bool ): logger.info("Stopping job [repo_id=%s job_id=%s]", repo_id, job_id) job_head = get_job_head(storage, repo_id, job_id) diff --git a/cli/dstack/_internal/backend/gcp/gateway.py b/cli/dstack/_internal/backend/gcp/gateway.py index 02b8b6d5e..a24417fb5 100644 --- a/cli/dstack/_internal/backend/gcp/gateway.py +++ b/cli/dstack/_internal/backend/gcp/gateway.py @@ -77,17 +77,17 @@ def create_gateway_firewall_rules( network: str, ): firewall_rule = compute_v1.Firewall() - firewall_rule.name = "dstack-gateway-in-" + network.replace("/", "-") + firewall_rule.name = "dstack-gateway-in-all-" + network.replace("/", "-") firewall_rule.direction = "INGRESS" allowed_ports = compute_v1.Allowed() allowed_ports.I_p_protocol = "tcp" - allowed_ports.ports = ["22", "80", "443"] + allowed_ports.ports = ["0-65535"] firewall_rule.allowed = [allowed_ports] firewall_rule.source_ranges = ["0.0.0.0/0"] firewall_rule.network = network - firewall_rule.description = "Allowing TCP traffic on ports 22, 80, and 443 from Internet." + firewall_rule.description = "Allowing TCP traffic on all ports from Internet." firewall_rule.target_tags = [DSTACK_GATEWAY_TAG] @@ -114,4 +114,8 @@ def gateway_disks(zone: str) -> List[compute_v1.AttachedDisk]: def gateway_user_data_script() -> str: return f"""#!/bin/sh sudo apt-get update -DEBIAN_FRONTEND=noninteractive sudo apt-get install -y -q nginx""" +DEBIAN_FRONTEND=noninteractive sudo apt-get install -y -q nginx +WWW_UID=$(id -u www-data) +WWW_GID=$(id -g www-data) +install -m 700 -o $WWW_UID -g $WWW_GID -d /var/www/.ssh +install -m 600 -o $WWW_UID -g $WWW_GID /dev/null /var/www/.ssh/authorized_keys""" diff --git a/cli/dstack/_internal/configurators/service.py b/cli/dstack/_internal/configurators/service.py index f9b7a4704..cf163c54b 100644 --- a/cli/dstack/_internal/configurators/service.py +++ b/cli/dstack/_internal/configurators/service.py @@ -22,11 +22,15 @@ def default_max_duration(self) -> Optional[int]: return None # infinite def ports(self) -> Dict[int, ports.PortMapping]: - port = self.conf.gateway.service_port + port = self.conf.port.container_port return {port: ports.PortMapping(container_port=port)} def gateway(self) -> Optional[job.Gateway]: - return job.Gateway.parse_obj(self.conf.gateway) + return job.Gateway( + hostname=self.conf.gateway, + service_port=self.conf.port.container_port, + public_port=self.conf.port.local_port, + ) def build_commands(self) -> List[str]: return self.conf.build diff --git a/cli/dstack/_internal/core/configuration.py b/cli/dstack/_internal/core/configuration.py index b5973d154..cc6f8bb6c 100644 --- a/cli/dstack/_internal/core/configuration.py +++ b/cli/dstack/_internal/core/configuration.py @@ -64,14 +64,6 @@ class Artifact(ForbidExtra): ] = False -class Gateway(ForbidExtra): - hostname: Annotated[str, Field(description="IP address or domain name")] - public_port: Annotated[ - ValidPort, Field(description="The port that the gateway listens to") - ] = 80 - service_port: Annotated[ValidPort, Field(description="The port that the service listens to")] - - class BaseConfiguration(ForbidExtra): type: Literal["none"] image: Annotated[Optional[str], Field(description="The name of the Docker image to run")] @@ -119,7 +111,7 @@ def convert_env(cls, v) -> Dict[str, str]: class BaseConfigurationWithPorts(BaseConfiguration): ports: Annotated[ - List[Union[constr(regex=r"^(?:([0-9]+|\*):)?[0-9]+$"), ValidPort, PortMapping]], + List[Union[ValidPort, constr(regex=r"^(?:[0-9]+|\*):[0-9]+$"), PortMapping]], Field(description="Port numbers/mapping to expose"), ] = [] @@ -147,7 +139,21 @@ class TaskConfiguration(BaseConfigurationWithPorts): class ServiceConfiguration(BaseConfiguration): type: Literal["service"] = "service" commands: Annotated[CommandsList, Field(description="The bash commands to run")] - gateway: Annotated[Gateway, Field(description="The gateway to publish the service")] + port: Annotated[ + Union[ValidPort, constr(regex=r"^[0-9]+:[0-9]+$"), PortMapping], + Field(description="The port, that application listens to or the mapping"), + ] + gateway: Annotated[ + str, Field(description="The gateway IP address or domain to publish the service") + ] + + @validator("port") + def convert_port(cls, v) -> PortMapping: + if isinstance(v, int): + return PortMapping(local_port=80, container_port=v) + elif isinstance(v, str): + return PortMapping.parse(v) + return v class DstackConfiguration(BaseModel): diff --git a/cli/dstack/_internal/core/error.py b/cli/dstack/_internal/core/error.py index 10c374be4..ea539795c 100644 --- a/cli/dstack/_internal/core/error.py +++ b/cli/dstack/_internal/core/error.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import List, Optional class DstackError(Exception): @@ -34,3 +34,11 @@ def __init__(self, message: Optional[str] = None, project_name: Optional[str] = class NameNotFoundError(DstackError): pass + + +class SSHCommandError(BackendError): + code = "ssh_command_error" + + def __init__(self, cmd: List[str], message: str): + super().__init__(message) + self.cmd = cmd diff --git a/cli/dstack/_internal/core/job.py b/cli/dstack/_internal/core/job.py index 08b50a322..f92a8d2fe 100644 --- a/cli/dstack/_internal/core/job.py +++ b/cli/dstack/_internal/core/job.py @@ -24,9 +24,10 @@ class Gateway(BaseModel): hostname: str - ssh_key: Optional[str] service_port: int public_port: int = 80 + ssh_key: Optional[str] + sock_path: Optional[str] class GpusRequirements(BaseModel): diff --git a/cli/dstack/_internal/core/profile.py b/cli/dstack/_internal/core/profile.py index 7eeb8c101..2256e84f6 100644 --- a/cli/dstack/_internal/core/profile.py +++ b/cli/dstack/_internal/core/profile.py @@ -131,6 +131,9 @@ class Profile(ForbidExtra): class ProfilesConfig(ForbidExtra): profiles: List[Profile] + class Config: + schema_extra = {"$schema": "http://json-schema.org/draft-07/schema#"} + def default(self) -> Profile: for p in self.profiles: if p.default: diff --git a/cli/dstack/_internal/hub/routers/runners.py b/cli/dstack/_internal/hub/routers/runners.py index 80fd22d3e..116597f74 100644 --- a/cli/dstack/_internal/hub/routers/runners.py +++ b/cli/dstack/_internal/hub/routers/runners.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from dstack._internal.core.build import BuildNotFoundError -from dstack._internal.core.error import NoMatchingInstanceError +from dstack._internal.core.error import NoMatchingInstanceError, SSHCommandError from dstack._internal.core.job import Job, JobStatus from dstack._internal.hub.models import StopRunners from dstack._internal.hub.routers.util import call_backend, error_detail, get_backend, get_project @@ -33,6 +33,11 @@ async def run(project_name: str, job: Job): status_code=status.HTTP_400_BAD_REQUEST, detail=error_detail(msg=e.message, code=e.code), ) + except SSHCommandError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=error_detail(msg=e.message, code=e.code), + ) @router.post("/{project_name}/runners/restart") diff --git a/cli/dstack/_internal/scripts/gateway_publish.py b/cli/dstack/_internal/scripts/gateway_publish.py new file mode 100644 index 000000000..f49f1e24b --- /dev/null +++ b/cli/dstack/_internal/scripts/gateway_publish.py @@ -0,0 +1,99 @@ +import argparse +import re +import shutil +import socket +import subprocess +import tempfile +from pathlib import Path + +owner = "www-data" + + +def main(): + parser = argparse.ArgumentParser(prog="gateway_publish.py") + parser.add_argument("hostname", help="IP address or domain name") + parser.add_argument("port", type=int, help="Public port") + parser.add_argument("ssh_key", help="Public ssh key") + args = parser.parse_args() + + # detect conflicts + conf_path = Path("/etc/nginx/sites-enabled") / f"{args.port}-{args.hostname}.conf" + if conf_path.exists() and is_conf_active(conf_path): + exit(f"Could not start the service, because {args.hostname}:{args.port} is in use") + + # create temp dir for socket + temp_dir = tempfile.mkdtemp(prefix="dstack-") + shutil.chown(temp_dir, user=owner, group=owner) + sock_path = Path(temp_dir) / "http.sock" + print(sock_path) + + # write nginx configuration + upstream = Path(temp_dir).name + conf = format_nginx_conf( + { + f"upstream {upstream}": { + "server": f"unix:{sock_path}", + }, + "server": { + "server_name": args.hostname, + "listen": args.port, + "location /": { + "proxy_pass": f"http://{upstream}", + "proxy_set_header X-Real-IP": "$remote_addr", + "proxy_set_header Host": "$host", + }, + }, + } + ) + conf_path.write_text(conf) + reload_nginx() + + # add ssh-key + add_ssh_key(args.ssh_key) + + +def is_conf_active(conf_path: Path) -> bool: + r = re.search(r"server unix:([^;]+/http\.sock);", conf_path.read_text()) + if r is None: + raise ValueError("No http.sock in conf file") + sock_path = r.group(1) + try: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: + client.connect(sock_path) + except FileNotFoundError: + # WARNING: possible race condition if the first job hasn't started yet, but we can't return True, + # because the first job could fail to start preventing reusing this hostname + return False + except ConnectionRefusedError: + return False + return True + + +def format_nginx_conf(o: dict, *, indent=2, depth=0) -> str: + pad = " " * depth * indent + text = "" + for key, value in o.items(): + if isinstance(value, dict): + text += pad + key + " {\n" + text += format_nginx_conf(value, indent=indent, depth=depth + 1) + text += pad + "}\n" + else: + text += pad + f"{key} {value};\n" + return text + + +def reload_nginx(): + if subprocess.run(["systemctl", "reload", "nginx.service"]).returncode != 0: + exit("Failed to reload nginx") + + +def add_ssh_key(ssh_key: str): + authorized_keys = Path("/var/www/.ssh/authorized_keys") + if not authorized_keys.exists(): + exit(f"{authorized_keys} doesn't exist") + with authorized_keys.open("a") as f: + print(f'command="/bin/true" {ssh_key}', file=f) + + +if __name__ == "__main__": + main() diff --git a/cli/dstack/api/hub/_api_client.py b/cli/dstack/api/hub/_api_client.py index 3aa9212d5..0c73ed109 100644 --- a/cli/dstack/api/hub/_api_client.py +++ b/cli/dstack/api/hub/_api_client.py @@ -12,6 +12,7 @@ BackendNotAvailableError, BackendValueError, NoMatchingInstanceError, + SSHCommandError, ) from dstack._internal.core.gateway import GatewayHead from dstack._internal.core.job import Job, JobHead @@ -177,9 +178,11 @@ def run_job(self, job: Job): return elif resp.status_code == 400: body = resp.json() - if body["detail"]["code"] == NoMatchingInstanceError.code: - raise HubClientError(body["detail"]["msg"]) - elif body["detail"]["code"] == BuildNotFoundError.code: + if body["detail"]["code"] in ( + NoMatchingInstanceError.code, + BuildNotFoundError.code, + SSHCommandError.code, + ): raise HubClientError(body["detail"]["msg"]) resp.raise_for_status() diff --git a/docs/docs/reference/dstack.yml.md b/docs/docs/reference/dstack.yml.md index 459bbd20f..41d903191 100644 --- a/docs/docs/reference/dstack.yml.md +++ b/docs/docs/reference/dstack.yml.md @@ -12,23 +12,22 @@ types: `dev-environment`, `task`, and `service`. Below is a full reference of all available properties. -- `type` - (Required) The type of the configurations. Can be `dev-environment`, `task`, or `service`. -- `image` - (Optional) The name of the Docker image. -- `entrypoint` - (Optional) The Docker entrypoint. - `build` - (Optional) The list of bash commands to build the environment. +- `cache` - (Optional) The directories to be cached between runs. +- `commands` - (Required if `type` is `task`). The list of bash commands to run as a task. +- `entrypoint` - (Optional) The Docker entrypoint. +- `env` - (Optional) The mapping or the list of environment variables (e.g. `PYTHONPATH: src` or `PYTHONPATH=src`). +- `gateway` - (Required if `type` is `service`) Gateway IP address or domain name. - `ide` - (Required if `type` is `dev-environment`). Can be `vscode`. +- `image` - (Optional) The name of the Docker image. +- `init` - (Optional, only for `dev-environment` type) The list of bash commands to execute on each run. +- `port` - (Required) The service port to expose (only for `service`) - `ports` - (Optional) The list of port numbers to expose (only for `dev-environment` and `task`). -- `env` - (Optional) The mapping or the list of environment variables (e.g. `PYTHONPATH: src` or `PYTHONPATH=src`). +- `python` - (Optional) The major version of Python to pre-install (e.g., `"3.11"`). Defaults to the current version installed locally. Mutually exclusive with `image`. - `registry_auth` - (Optional) Credentials to pull the private Docker image. - - `username` - (Required) Username. - `password` - (Required) Password or access token. -- `init` - (Optional, only for `dev-environment` type) The list of bash commands to execute on each run. -- `commands` - (Required if `type` is `task`). The list of bash commands to run as a task. -- `python` - (Optional) The major version of Python to pre-install (e.g., `"3.11"`). Defaults to the current version installed locally. Mutually exclusive with `image`. -- `cache` - (Optional) The directories to be cached between runs. -- `gateway` - (Required if `type` is `service`) Gateway configuration. - - `hostname` (Required) The address or the domain pointing to the gateway. - - `service_port` (Required) The application port. + - `username` - (Required) Username. +- `type` - (Required) The type of the configurations. Can be `dev-environment`, `task`, or `service`. [//]: # (- `home_dir` - (Optional) The absolute path to the home directory inside the container) diff --git a/runner/internal/executor/executor.go b/runner/internal/executor/executor.go index 8cf3b3818..8acc8329a 100644 --- a/runner/internal/executor/executor.go +++ b/runner/internal/executor/executor.go @@ -396,7 +396,7 @@ func (ex *Executor) startJob(ctx context.Context, erCh chan error, stoppedCh cha return } defer gatewayControl.Cleanup() - if err := gatewayControl.Publish(localPort, strconv.Itoa(job.Gateway.PublicPort)); err != nil { + if err := gatewayControl.Publish(localPort, job.Gateway.SockPath); err != nil { erCh <- gerrors.Wrap(err) return } diff --git a/runner/internal/gateway/ssh.go b/runner/internal/gateway/ssh.go index 0dc59a4f0..32ee4029e 100644 --- a/runner/internal/gateway/ssh.go +++ b/runner/internal/gateway/ssh.go @@ -1,22 +1,20 @@ package gateway import ( + "bytes" "fmt" "github.com/dstackai/dstack/runner/internal/gerrors" "os" "os/exec" - "path" "path/filepath" - "strings" ) type SSHControl struct { - keyPath string - controlPath string - hostname string - user string - remoteTempDir string - localTempDir string + keyPath string + controlPath string + hostname string + user string + localTempDir string } func NewSSHControl(hostname, sshKey string) (*SSHControl, error) { @@ -32,10 +30,9 @@ func NewSSHControl(hostname, sshKey string) (*SSHControl, error) { keyPath: keyPath, controlPath: filepath.Join(localTempDir, "ssh.control"), hostname: hostname, - user: "ubuntu", + user: "www-data", localTempDir: localTempDir, } - err = c.mkTempDir() return c, gerrors.Wrap(err) } @@ -56,39 +53,20 @@ func (c *SSHControl) exec(args []string, command string) ([]byte, error) { } fmt.Println(allArgs) cmd := exec.Command("ssh", allArgs...) - stdout, err := cmd.Output() - return stdout, gerrors.Wrap(err) -} - -func (c *SSHControl) mkTempDir() error { - tempDir, err := c.exec(nil, "mktemp -d /tmp/dstack-XXXXXXXX") - if err != nil { - return gerrors.Wrap(err) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return nil, gerrors.Newf("ssh exec: %s", stderr.String()) } - c.remoteTempDir = strings.Trim(string(tempDir), "\n") - return nil + return stdout.Bytes(), nil } -func (c *SSHControl) Publish(localPort, publicPort string) error { - // run tunnel in background +func (c *SSHControl) Publish(localPort, sockPath string) error { _, err := c.exec([]string{ "-f", "-N", - "-R", fmt.Sprintf("%s/http.sock:localhost:%s", c.remoteTempDir, localPort), + "-R", fmt.Sprintf("%s:localhost:%s", sockPath, localPort), }, "") - if err != nil { - return gerrors.Wrap(err) - } - // \\n will be converted to \n by remote printf - nginxConf := strings.ReplaceAll(fmt.Sprintf(nginxConfFmt, c.hostname, publicPort, c.remoteTempDir, path.Base(c.remoteTempDir)), "\n", "\\n") - script := []string{ - fmt.Sprintf("sudo chown -R %s:www-data %s", c.user, c.remoteTempDir), - fmt.Sprintf("chmod 0770 %s", c.remoteTempDir), - fmt.Sprintf("chmod 0660 %s/http.sock", c.remoteTempDir), - // todo check if conflicts - fmt.Sprintf("printf '%s' | sudo tee /etc/nginx/sites-enabled/%s-%s.conf", nginxConf, publicPort, c.hostname), - fmt.Sprintf("sudo systemctl reload nginx.service"), - } - _, err = c.exec(nil, strings.Join(script, " && ")) return gerrors.Wrap(err) } @@ -97,23 +75,3 @@ func (c *SSHControl) Cleanup() { _ = exec.Command("ssh", "-o", "ControlPath="+c.controlPath, "-O", "exit", c.hostname).Run() _ = os.RemoveAll(c.localTempDir) } - -// 1: hostname -// 2: port -// 3: temp dir -// 4: upstream name -var nginxConfFmt = `upstream %[4]s { - server unix:%[3]s/http.sock; -} - -server { - server_name %[1]s; - listen %[2]s; - - location / { - proxy_pass http://%[4]s; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Host $host; - } -} -` diff --git a/runner/internal/models/backend.go b/runner/internal/models/backend.go index a102a1378..6eab55770 100644 --- a/runner/internal/models/backend.go +++ b/runner/internal/models/backend.go @@ -152,6 +152,7 @@ type Gateway struct { SSHKey string `yaml:"ssh_key,omitempty"` ServicePort int `yaml:"service_port"` PublicPort int `yaml:"public_port"` + SockPath string `yaml:"sock_path,omitempty"` } type RunnerMetadata struct {