diff --git a/Taskfile.yaml b/Taskfile.yaml deleted file mode 100644 index e22c7dfac..000000000 --- a/Taskfile.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: "3" - -includes: - dev-tunnel: - taskfile: infra/dev-tunnel - dir: infra/dev-tunnel diff --git a/infra/dev-tunnel/Taskfile.yaml b/infra/dev-tunnel/Taskfile.yaml deleted file mode 100644 index cff78049b..000000000 --- a/infra/dev-tunnel/Taskfile.yaml +++ /dev/null @@ -1,21 +0,0 @@ -version: "3" - -tasks: - init: - internal: true - cmds: - - terraform init -upgrade - - up: - deps: [init] - cmds: - - terraform apply - - down: - deps: [init] - cmds: - - terraform destroy - - get-ip: - cmds: - - terraform output diff --git a/infra/dev-tunnel/providers.tf b/infra/dev-tunnel/providers.tf deleted file mode 100644 index 251199e2f..000000000 --- a/infra/dev-tunnel/providers.tf +++ /dev/null @@ -1,3 +0,0 @@ -provider "linode" { - token = var.linode_token -} \ No newline at end of file diff --git a/infra/dev-tunnel/vars.tf b/infra/dev-tunnel/vars.tf deleted file mode 100644 index 04a11cc34..000000000 --- a/infra/dev-tunnel/vars.tf +++ /dev/null @@ -1,4 +0,0 @@ -variable "linode_token" { - type = string - sensitive = true -} \ No newline at end of file diff --git a/infra/tf/dev_tunnel/dev_tunnel b/infra/tf/dev_tunnel/dev_tunnel new file mode 120000 index 000000000..fb427df1f --- /dev/null +++ b/infra/tf/dev_tunnel/dev_tunnel @@ -0,0 +1 @@ +dev_tunnel/ \ No newline at end of file diff --git a/infra/dev-tunnel/main.tf b/infra/tf/dev_tunnel/main.tf similarity index 72% rename from infra/dev-tunnel/main.tf rename to infra/tf/dev_tunnel/main.tf index 8cc33997a..104287146 100644 --- a/infra/dev-tunnel/main.tf +++ b/infra/tf/dev_tunnel/main.tf @@ -11,6 +11,11 @@ terraform { } } -output "ip" { - value = linode_instance.tunnel.ip_address +module "secrets" { + source = "../modules/secrets" + + keys = [ + "linode/token", + ] } + diff --git a/infra/tf/dev_tunnel/outputs.tf b/infra/tf/dev_tunnel/outputs.tf new file mode 100644 index 000000000..a228d208c --- /dev/null +++ b/infra/tf/dev_tunnel/outputs.tf @@ -0,0 +1,3 @@ +output "tunnel_public_ip" { + value = linode_instance.tunnel.ip_address +} diff --git a/infra/tf/dev_tunnel/providers.tf b/infra/tf/dev_tunnel/providers.tf new file mode 100644 index 000000000..0c6d7aa3f --- /dev/null +++ b/infra/tf/dev_tunnel/providers.tf @@ -0,0 +1,3 @@ +provider "linode" { + token = module.secrets.values["linode/token"] +} diff --git a/infra/dev-tunnel/server.tf b/infra/tf/dev_tunnel/server.tf similarity index 57% rename from infra/dev-tunnel/server.tf rename to infra/tf/dev_tunnel/server.tf index e01a59c43..203deba68 100644 --- a/infra/dev-tunnel/server.tf +++ b/infra/tf/dev_tunnel/server.tf @@ -1,5 +1,5 @@ locals { - dev_tunnel_name = "dev-tunnel-${random_string.tunnel_suffix.result}" + dev_tunnel_name = "${var.namespace}-dev-tunnel" } resource "random_string" "tunnel_suffix" { @@ -11,9 +11,9 @@ resource "random_string" "tunnel_suffix" { } resource "random_password" "password" { - length = 16 - special = true - override_special = "_%@" + length = 16 + special = true + override_special = "_%@" } resource "linode_instance" "tunnel" { @@ -23,7 +23,7 @@ resource "linode_instance" "tunnel" { type = "g6-nanode-1" authorized_keys = [trimspace(tls_private_key.ssh_key.public_key_openssh)] root_pass = random_password.password.result - tags = ["dev-tunnel"] + tags = ["rivet-${var.namespace}", "${var.namespace}-dev-tunnel"] } resource "linode_firewall" "tunnel_firewall" { @@ -45,38 +45,44 @@ resource "linode_firewall" "tunnel_firewall" { label = "http" action = "ACCEPT" protocol = "TCP" - ports = "80" + ports = var.api_http_port ipv4 = ["0.0.0.0/0"] ipv6 = ["::/0"] } - inbound { - label = "https" - action = "ACCEPT" - protocol = "TCP" - ports = "443" - ipv4 = ["0.0.0.0/0"] - ipv6 = ["::/0"] + dynamic "inbound" { + for_each = var.api_https_port != null ? [1] : [] + content { + label = "https" + action = "ACCEPT" + protocol = "TCP" + ports = var.api_https_port + ipv4 = ["0.0.0.0/0"] + ipv6 = ["::/0"] + } } inbound { label = "tunnel" action = "ACCEPT" protocol = "TCP" - ports = "5000" + ports = var.tunnel_port ipv4 = ["0.0.0.0/0"] ipv6 = ["::/0"] } - inbound { - label = "minio" - action = "ACCEPT" - protocol = "TCP" - ports = "9000" - ipv4 = ["0.0.0.0/0"] - ipv6 = ["::/0"] + dynamic "inbound" { + for_each = var.minio_port != null ? [1] : [] + content { + label = "minio" + action = "ACCEPT" + protocol = "TCP" + ports = var.minio_port + ipv4 = ["0.0.0.0/0"] + ipv6 = ["::/0"] + } } - linodes = [linode_instance.tunnel.id] + linodes = [linode_instance.tunnel.id] } - \ No newline at end of file + diff --git a/infra/dev-tunnel/tls.tf b/infra/tf/dev_tunnel/tls.tf similarity index 100% rename from infra/dev-tunnel/tls.tf rename to infra/tf/dev_tunnel/tls.tf diff --git a/infra/dev-tunnel/tunnel.tf b/infra/tf/dev_tunnel/tunnel.tf similarity index 77% rename from infra/dev-tunnel/tunnel.tf rename to infra/tf/dev_tunnel/tunnel.tf index 7df6b75f8..0fe657296 100644 --- a/infra/dev-tunnel/tunnel.tf +++ b/infra/tf/dev_tunnel/tunnel.tf @@ -1,3 +1,14 @@ +locals { + fwd_ports = flatten([ + var.api_http_port, + var.api_https_port != null ? [var.api_https_port] : [], + var.tunnel_port, + var.minio_port != null ? [var.minio_port] : [], + ]) + + ssh_fwd_flags = join(" ", [for x in local.fwd_ports: "-R 0.0.0.0:${x}:127.0.0.1:${x}"]) +} + resource "null_resource" "update_sshd_config" { depends_on = [linode_instance.tunnel] triggers = { @@ -47,7 +58,7 @@ resource "docker_container" "ssh_tunnel" { apt-get install -y openssh-client while true; do echo 'Connecting...' - ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa -vNT -R 0.0.0.0:80:127.0.0.1:80 -R 0.0.0.0:443:127.0.0.1:443 -R 0.0.0.0:5000:127.0.0.1:5000 -R 0.0.0.0:9000:127.0.0.1:9000 root@${linode_instance.tunnel.ip_address} + ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa -vNT ${local.ssh_fwd_flags} root@${linode_instance.tunnel.ip_address} sleep 5 done EOF diff --git a/infra/tf/dev_tunnel/vars.tf b/infra/tf/dev_tunnel/vars.tf new file mode 100644 index 000000000..0d29688b2 --- /dev/null +++ b/infra/tf/dev_tunnel/vars.tf @@ -0,0 +1,22 @@ +variable "namespace" { + type = string +} + +variable "api_http_port" { + type = number +} + +variable "api_https_port" { + type = number + nullable = true +} + +variable "minio_port" { + type = number + nullable = true +} + +variable "tunnel_port" { + type = number +} + diff --git a/infra/tf/k8s_cluster_k3d/output.tf b/infra/tf/k8s_cluster_k3d/output.tf index 3bce75352..90d284f80 100644 --- a/infra/tf/k8s_cluster_k3d/output.tf +++ b/infra/tf/k8s_cluster_k3d/output.tf @@ -1,7 +1,3 @@ -output "traefik_external_ip" { - value = var.public_ip -} - output "repo_host" { value = local.repo_host } diff --git a/infra/tf/k8s_cluster_k3d/vars.tf b/infra/tf/k8s_cluster_k3d/vars.tf index 1a08a35c8..900154c02 100644 --- a/infra/tf/k8s_cluster_k3d/vars.tf +++ b/infra/tf/k8s_cluster_k3d/vars.tf @@ -14,10 +14,6 @@ variable "cargo_target_dir" { type = string } -variable "public_ip" { - type = string -} - variable "api_http_port" { type = number } diff --git a/infra/tf/k8s_infra/outputs.tf b/infra/tf/k8s_infra/outputs.tf index 516ecae40..cd712595e 100644 --- a/infra/tf/k8s_infra/outputs.tf +++ b/infra/tf/k8s_infra/outputs.tf @@ -2,14 +2,16 @@ output "traefik_external_ip" { value = ( var.deploy_method_cluster ? data.kubernetes_service.traefik.status[0].load_balancer[0].ingress[0].hostname : - var.public_ip + var.dev_public_ip ) } output "traefik_tunnel_external_ip" { value = ( - var.deploy_method_cluster && var.edge_enabled ? - data.kubernetes_service.traefik_tunnel.0.status[0].load_balancer[0].ingress[0].hostname : - var.public_ip + var.edge_enabled + ? var.deploy_method_cluster + ? data.kubernetes_service.traefik_tunnel.0.status[0].load_balancer[0].ingress[0].hostname + : var.dev_public_ip + : null ) } diff --git a/infra/tf/k8s_infra/vars.tf b/infra/tf/k8s_infra/vars.tf index dab91aace..161607b76 100644 --- a/infra/tf/k8s_infra/vars.tf +++ b/infra/tf/k8s_infra/vars.tf @@ -6,7 +6,7 @@ variable "deploy_method_cluster" { type = bool } -variable "public_ip" { +variable "dev_public_ip" { type = string nullable = true default = null diff --git a/lib/bolt/config/src/ns.rs b/lib/bolt/config/src/ns.rs index 2e3dcbea3..60087becd 100644 --- a/lib/bolt/config/src/ns.rs +++ b/lib/bolt/config/src/ns.rs @@ -66,7 +66,9 @@ pub struct Cluster { pub enum ClusterKind { #[serde(rename = "single_node")] SingleNode { - public_ip: String, + #[serde(default)] + public_ip: Option, + /// Port to expose API HTTP interface. Exposed on public IP. #[serde(default = "default_api_http_port")] api_http_port: u16, @@ -85,11 +87,19 @@ pub enum ClusterKind { /// Disabled by default since this doesn't play well with development machines. #[serde(default)] limit_resources: bool, + + /// Create a dev tunnel for this server. + #[serde(default)] + dev_tunnel: Option, }, #[serde(rename = "distributed")] Distributed {}, } +#[derive(Default, Serialize, Deserialize, Clone, Debug)] +#[serde(deny_unknown_fields)] +pub struct DevTunnel {} + #[derive(Default, Serialize, Deserialize, Clone, Debug)] #[serde(deny_unknown_fields)] pub struct Secrets { diff --git a/lib/bolt/core/src/context/project.rs b/lib/bolt/core/src/context/project.rs index 033aaf4c1..8f542bbbf 100644 --- a/lib/bolt/core/src/context/project.rs +++ b/lib/bolt/core/src/context/project.rs @@ -40,12 +40,12 @@ impl ProjectContextData { } pub async fn openapi_config_cloud( - &self, + self: &Arc, ) -> Result { let api_admin_token = self.read_secret(&["rivet", "api_admin", "token"]).await?; Ok(rivet_api::apis::configuration::Configuration { - base_path: self.origin_api(), + base_path: self.origin_api().await, bearer_access_token: Some(api_admin_token), ..Default::default() }) @@ -803,17 +803,53 @@ impl ProjectContextData { self.domain_main().map(|x| format!("api.{x}")) } + pub fn has_dev_tunnel(&self) -> bool { + matches!( + self.ns().cluster.kind, + config::ns::ClusterKind::SingleNode { + dev_tunnel: Some(_), + .. + } + ) + } + + pub async fn get_dev_public_ip(self: &Arc) -> String { + match &self.ns().cluster.kind { + config::ns::ClusterKind::SingleNode { + public_ip: Some(_), + dev_tunnel: Some(_), + .. + } => { + panic!("cannot have both public_ip and dev_tunnel") + } + config::ns::ClusterKind::SingleNode { + public_ip: Some(public_ip), + .. + } => public_ip.clone(), + config::ns::ClusterKind::SingleNode { + dev_tunnel: Some(_), + .. + } => { + let dev_tunnel = terraform::output::read_dev_tunnel(self).await; + (*dev_tunnel.tunnel_public_ip).clone() + } + config::ns::ClusterKind::SingleNode { .. } => { + panic!("public ip not configured") + } + config::ns::ClusterKind::Distributed { .. } => { + panic!("does not have dev public ip") + } + } + } + /// Host used for building links to the API endpoint. - pub fn host_api(&self) -> String { + pub async fn host_api(self: &Arc) -> String { if let Some(domain_main_api) = self.domain_main_api() { domain_main_api - } else if let config::ns::ClusterKind::SingleNode { - public_ip, - api_http_port, - .. - } = &self.ns().cluster.kind + } else if let config::ns::ClusterKind::SingleNode { api_http_port, .. } = + &self.ns().cluster.kind { - format!("{public_ip}:{api_http_port}") + format!("{}:{api_http_port}", self.get_dev_public_ip().await) } else { unreachable!() } @@ -834,32 +870,26 @@ impl ProjectContextData { } /// Origin used for building links to the API endpoint. - pub fn origin_api(&self) -> String { + pub async fn origin_api(self: &Arc) -> String { if let Some(domain_main_api) = self.domain_main_api() { format!("https://{domain_main_api}") - } else if let config::ns::ClusterKind::SingleNode { - public_ip, - api_http_port, - .. - } = &self.ns().cluster.kind + } else if let config::ns::ClusterKind::SingleNode { api_http_port, .. } = + &self.ns().cluster.kind { - format!("http://{public_ip}:{api_http_port}") + format!("http://{}:{api_http_port}", self.get_dev_public_ip().await) } else { unreachable!() } } /// Origin used to access Minio. Only applicable if Minio provider is specified. - pub fn origin_minio(&self) -> String { + pub async fn origin_minio(self: &Arc) -> String { if let Some(domain_main) = self.domain_main() { format!("https://minio.{domain_main}") - } else if let config::ns::ClusterKind::SingleNode { - public_ip, - minio_port, - .. - } = &self.ns().cluster.kind + } else if let config::ns::ClusterKind::SingleNode { minio_port, .. } = + &self.ns().cluster.kind { - format!("http://{public_ip}:{minio_port}") + format!("http://{}:{minio_port}", self.get_dev_public_ip().await) } else { unreachable!() } @@ -984,7 +1014,7 @@ impl ProjectContextData { Ok(S3Config { endpoint_internal: "http://minio.minio.svc.cluster.local:9000".into(), // Use localhost if DNS is not configured - endpoint_external: self.origin_minio(), + endpoint_external: self.origin_minio().await, // Minio defaults to us-east-1 region // https://github.com/minio/minio/blob/0ec722bc5430ad768a263b8464675da67330ad7c/cmd/server-main.go#L739 region: "us-east-1".into(), diff --git a/lib/bolt/core/src/context/service.rs b/lib/bolt/core/src/context/service.rs index 13698f925..72d07dfd9 100644 --- a/lib/bolt/core/src/context/service.rs +++ b/lib/bolt/core/src/context/service.rs @@ -831,9 +831,9 @@ impl ServiceContextData { { env.insert("RIVET_SUPPORT_DEPRECATED_SUBDOMAINS".into(), "1".into()); } - env.insert("RIVET_HOST_API".into(), project_ctx.host_api()); + env.insert("RIVET_HOST_API".into(), project_ctx.host_api().await); env.insert("RIVET_HOST_TUNNEL".into(), project_ctx.host_tunnel().await); - env.insert("RIVET_ORIGIN_API".into(), project_ctx.origin_api()); + env.insert("RIVET_ORIGIN_API".into(), project_ctx.origin_api().await); env.insert("RIVET_ORIGIN_HUB".into(), project_ctx.origin_hub()); // DNS @@ -1436,7 +1436,7 @@ async fn add_s3_env( if let ( s3_util::Provider::Minio, config::ns::ClusterKind::SingleNode { - public_ip, + public_ip: Some(public_ip), minio_port, .. }, diff --git a/lib/bolt/core/src/dep/terraform/gen.rs b/lib/bolt/core/src/dep/terraform/gen.rs index 6636a49a4..acb9fd787 100644 --- a/lib/bolt/core/src/dep/terraform/gen.rs +++ b/lib/bolt/core/src/dep/terraform/gen.rs @@ -151,7 +151,6 @@ async fn vars(ctx: &ProjectContext) { match &config.cluster.kind { ns::ClusterKind::SingleNode { - public_ip, api_http_port, api_https_port, minio_port, @@ -160,11 +159,12 @@ async fn vars(ctx: &ProjectContext) { } => { vars.insert("deploy_method_local".into(), json!(true)); vars.insert("deploy_method_cluster".into(), json!(false)); - vars.insert("public_ip".into(), json!(public_ip)); vars.insert("api_http_port".into(), json!(api_http_port)); vars.insert("api_https_port".into(), json!(api_https_port)); vars.insert("tunnel_port".into(), json!(tunnel_port)); + vars.insert("dev_public_ip".into(), json!(ctx.get_dev_public_ip().await)); + // Expose Minio on a dedicated port if DNS not enabled if config.dns.is_none() && config.s3.providers.minio.is_some() { vars.insert("minio_port".into(), json!(minio_port)); @@ -554,13 +554,14 @@ async fn vars(ctx: &ProjectContext) { }; // Create monitors + let origin_api = ctx.origin_api().await; let api_status_monitors = cluster .datacenters .iter() .map(|(name_id, dc)| { json!({ "id": dc.datacenter_id, - "url": format!("{}/status/matchmaker?region={}", ctx.origin_api(), name_id), + "url": format!("{}/status/matchmaker?region={}", origin_api, name_id), "public_name": dc.display_name, }) }) diff --git a/lib/bolt/core/src/dep/terraform/output.rs b/lib/bolt/core/src/dep/terraform/output.rs index a99f58281..3181f944c 100644 --- a/lib/bolt/core/src/dep/terraform/output.rs +++ b/lib/bolt/core/src/dep/terraform/output.rs @@ -23,6 +23,11 @@ pub struct Cert { pub key_pem: String, } +#[derive(Debug, Clone, Deserialize)] +pub struct DevTunnel { + pub tunnel_public_ip: TerraformOutputValue, +} + #[derive(Debug, Clone, Deserialize)] pub struct K8sInfra { pub traefik_tunnel_external_ip: TerraformOutputValue, @@ -90,6 +95,10 @@ pub struct Redis { pub port: TerraformOutputValue>, } +pub async fn read_dev_tunnel(ctx: &ProjectContext) -> DevTunnel { + read_plan::(ctx, "dev_tunnel").await +} + pub async fn read_k8s_infra(ctx: &ProjectContext) -> K8sInfra { read_plan::(ctx, "k8s_infra").await } diff --git a/lib/bolt/core/src/tasks/api.rs b/lib/bolt/core/src/tasks/api.rs index 64ea310f8..07a84a5ed 100644 --- a/lib/bolt/core/src/tasks/api.rs +++ b/lib/bolt/core/src/tasks/api.rs @@ -15,7 +15,7 @@ pub async fn access_token_login(project_ctx: &ProjectContext, name: String) -> R .read_secret(&["rivet", "api_admin", "token"]) .await?; let response = reqwest::Client::new() - .post(format!("{}/admin/login", project_ctx.origin_api(),)) + .post(format!("{}/admin/login", project_ctx.origin_api().await)) .bearer_auth(api_admin_token) .json(&json!({ "name": name, diff --git a/lib/bolt/core/src/tasks/infra/mod.rs b/lib/bolt/core/src/tasks/infra/mod.rs index 2907e6f0f..9ca35394b 100644 --- a/lib/bolt/core/src/tasks/infra/mod.rs +++ b/lib/bolt/core/src/tasks/infra/mod.rs @@ -103,6 +103,17 @@ pub fn build_plan( ) -> Result> { let mut plan = Vec::new(); + // Dev Tunnel + if ctx.has_dev_tunnel() { + plan.push(PlanStep { + name_id: "dev-tunnel", + kind: PlanStepKind::Terraform { + plan_id: "dev_tunnel".into(), + needs_destroy: true, + }, + }); + } + // Infra match ctx.ns().kubernetes.provider { ns::KubernetesProvider::K3d { .. } => { @@ -288,7 +299,7 @@ pub fn build_plan( // BetterUptime if ctx.ns().better_uptime.is_some() { plan.push(PlanStep { - name_id: "better_uptime", + name_id: "better-uptime", kind: PlanStepKind::Terraform { plan_id: "better_uptime".into(), needs_destroy: true,