import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; /** One-time provisioning; secrets stay in owner-only files or Gitea secret storage. */ class ReleaseSetup { static void configure(Path root, Map input) throws Exception { var env = new HashMap<>(input); env.putIfAbsent("GITEA_SERVER_URL", "http://192.168.2.28:3000"); env.putIfAbsent("CI_REPOSITORY", "wanjakrah/agiledancers"); env.putIfAbsent("DEPLOY_HOST", "192.168.2.28"); env.putIfAbsent("DEPLOY_USER", "agiledancers-deploy"); env.putIfAbsent("DEPLOY_SSH_PORT", "22"); env.putIfAbsent("DEPLOY_DIRECTORY", "/volume1/docker/agiledancers-production"); env.putIfAbsent("DEPLOY_BIND_IP", "192.168.2.28"); env.putIfAbsent("DEPLOY_FRONTEND_PORT", "8088"); env.putIfAbsent("DEPLOY_REGISTRY", "127.0.0.1:3000"); Path config = Setup.home().resolve(".config/agiledancers-runner"); env.putIfAbsent("GITEA_ADMIN_TOKEN_FILE", config.resolve("admin-token").toString()); env.putIfAbsent("RELEASE_TOKEN_FILE", config.resolve("s6-release-token").toString()); env.putIfAbsent("PULL_TOKEN_FILE", config.resolve("s6-pull-token").toString()); env.putIfAbsent("DEPLOY_SSH_KEY_FILE", config.resolve("s6-deploy-key").toString()); String admin = Setup.secret(env, "GITEA_ADMIN_TOKEN_FILE"); String pull = Setup.secret(env, "PULL_TOKEN_FILE"); env.put("RELEASE_TOKEN", Setup.secret(env, "RELEASE_TOKEN_FILE")); env.put("DEPLOY_SSH_KEY", Setup.secret(env, "DEPLOY_SSH_KEY_FILE")); String bind = env.get("DEPLOY_BIND_IP"), port = env.get("DEPLOY_FRONTEND_PORT"); Pipeline.check( bind.matches("[0-9.]+") && port.matches("[0-9]{4,5}") && Integer.parseInt(port) >= 1024 && Integer.parseInt(port) <= 65535, "Invalid private bind address or port"); var commands = new Commands(Duration.ofMinutes(15)); Path diagnostics = root.resolve("ci/artifacts/deployment"); Files.createDirectories(diagnostics); commands.log = diagnostics.resolve("setup.log"); Files.writeString(commands.log, ""); String host = env.get("DEPLOY_HOST"); String lookup = env.get("DEPLOY_SSH_PORT").equals("22") ? host : "[" + host + "]:" + env.get("DEPLOY_SSH_PORT"); env.put( "DEPLOY_KNOWN_HOSTS", commands.execute( List.of( "ssh-keygen", "-F", lookup, "-f", Setup.home().resolve(".ssh/known_hosts").toString()), root, env, null, false) .checked()); Pipeline.check(!env.get("DEPLOY_KNOWN_HOSTS").isBlank(), "A previously trusted NAS hostkey is required"); var api = new ReleaseApi(env); api.verified(api.head()); var uri = URI.create(api.server); String registry = env.get("DEPLOY_REGISTRY"); int registryPort = uri.getPort() >= 0 ? uri.getPort() : uri.getScheme().equals("https") ? 443 : 80; Pipeline.check( registry.equals(uri.getRawAuthority()) || registry.equals("127.0.0.1:" + registryPort), "Deployment registry must be the Gitea registry or its NAS loopback alias"); String registryUser = env.getOrDefault("PULL_USER", env.get("CI_REPOSITORY").split("/")[0]); Pipeline.check(registryUser.matches("[a-zA-Z0-9_.-]+"), "Invalid registry user"); try (var remote = new ReleaseRemote(root, env, commands, "setup")) { System.out.println(remote.executeScript( Files.readString(root.resolve("deploy/provision.sh")), pull + "\n", 300, List.of(remote.directory, registryUser, registry, bind, port))); remote.acquire(); try { remote.call("preflight", null); } finally { remote.release(); } } // Provision credentials only after successful NAS login and release-read verification. for (String secret : List.of("RELEASE_TOKEN", "DEPLOY_SSH_KEY", "DEPLOY_KNOWN_HOSTS")) Setup.api( api.server, admin, "PUT", api.base + "/actions/secrets/" + secret, Map.of("data", env.get(secret))); for (String variable : List.of("DEPLOY_HOST", "DEPLOY_USER", "DEPLOY_SSH_PORT", "DEPLOY_DIRECTORY", "DEPLOY_REGISTRY")) variable(api, admin, variable, env.get(variable)); System.out.println( "Production configuration and Gitea deployment secrets/variables installed; no release was created."); } static void variable(ReleaseApi api, String admin, String name, String value) throws Exception { String path = api.base + "/actions/variables/" + name; boolean exists; try { Setup.api(api.server, admin, "GET", path, null); exists = true; } catch (Setup.ApiFailure error) { if (error.status != 404) throw error; exists = false; } Setup.api(api.server, admin, exists ? "PUT" : "POST", path, Map.of("value", value)); } }