@@ -320,7 +320,9 @@ impl HostShared {
320320}
321321
322322const GATEWAY_CACHE_PATH : & str = "/run/dstack/gateway-cache.json" ;
323- const WG_CONFIG_PATH : & str = "/etc/wireguard/dstack-wg0.conf" ;
323+ /// Name of the WireGuard interface linking this CVM to dstack-gateway.
324+ pub const WG_INTERFACE : & str = "dstack-wg0" ;
325+ pub const WG_CONFIG_PATH : & str = "/etc/wireguard/dstack-wg0.conf" ;
324326/// Certificate validity period in seconds (10 days)
325327const CERT_VALIDITY_SECS : u64 = 10 * 24 * 3600 ;
326328const MAX_SUPPORTED_MANIFEST_VERSION : u32 = 3 ;
@@ -555,6 +557,14 @@ impl<'a> GatewayContext<'a> {
555557 // Get or generate key store (includes WireGuard keys and client certificate)
556558 let key_store = self . get_or_generate_key_store ( ) . await ?;
557559
560+ // Persist the key store before attempting registration. Minting it costs a
561+ // KMS round-trip, two cert signing requests and a TDX quote, so a gateway
562+ // outage would otherwise make every retry pay that price again and turn a
563+ // gateway outage into a KMS load spike across the whole fleet.
564+ if let Err ( e) = key_store. save ( ) {
565+ warn ! ( "failed to save gateway cache: {e:?}" ) ;
566+ }
567+
558568 if self . shared . sys_config . gateway_urls . is_empty ( ) {
559569 bail ! ( "Missing gateway urls" ) ;
560570 }
@@ -604,11 +614,6 @@ impl<'a> GatewayContext<'a> {
604614 ) ) ;
605615 }
606616
607- // Save cache
608- if let Err ( e) = key_store. save ( ) {
609- warn ! ( "Failed to save gateway cache: {e:?}" ) ;
610- }
611-
612617 // Check if config has changed (skip check if force is set)
613618 if !force {
614619 let current_config = fs:: read_to_string ( WG_CONFIG_PATH ) . ok ( ) ;
@@ -1931,19 +1936,61 @@ impl Stage0<'_> {
19311936 }
19321937}
19331938
1934- pub async fn cmd_gateway_refresh ( args : GatewayRefreshArgs ) -> Result < ( ) > {
1935- let host_shared_dir = args. work_dir . join ( HOST_SHARED_DIR_NAME ) ;
1936- let shared = HostShared :: load ( host_shared_dir. as_path ( ) ) . with_context ( || {
1937- format ! (
1938- "Failed to load host-shared dir: {}" ,
1939- host_shared_dir. display( )
1940- )
1941- } ) ?;
1942- let keys_path = shared. dir . join ( APP_KEYS ) ;
1943- let keys: AppKeys = deserialize_json_file ( & keys_path)
1944- . with_context ( || format ! ( "Failed to load app keys from {}" , keys_path. display( ) ) ) ?;
1939+ /// Owns the inputs needed to (re)register this CVM with dstack-gateway.
1940+ ///
1941+ /// Loading is separated from refreshing so a long-running caller (the gateway
1942+ /// checker) can pay the parsing cost once and then refresh repeatedly.
1943+ pub struct GatewayRefresher {
1944+ shared : HostShared ,
1945+ keys : AppKeys ,
1946+ }
1947+
1948+ impl GatewayRefresher {
1949+ /// Load the host-shared config and app keys from `work_dir`.
1950+ pub fn load ( work_dir : & Path ) -> Result < Self > {
1951+ let host_shared_dir = work_dir. join ( HOST_SHARED_DIR_NAME ) ;
1952+ let shared = HostShared :: load ( host_shared_dir. as_path ( ) ) . with_context ( || {
1953+ format ! (
1954+ "Failed to load host-shared dir: {}" ,
1955+ host_shared_dir. display( )
1956+ )
1957+ } ) ?;
1958+ let keys_path = shared. dir . join ( APP_KEYS ) ;
1959+ let keys: AppKeys = deserialize_json_file ( & keys_path)
1960+ . with_context ( || format ! ( "Failed to load app keys from {}" , keys_path. display( ) ) ) ?;
1961+ Ok ( Self { shared, keys } )
1962+ }
1963+
1964+ /// Whether this app opted into dstack-gateway at all.
1965+ pub fn gateway_enabled ( & self ) -> bool {
1966+ self . shared . app_compose . gateway_enabled ( )
1967+ }
19451968
1946- GatewayContext :: new ( & shared, & keys) . setup ( args. force ) . await
1969+ /// Validate the parts of the gateway config that can never become valid by
1970+ /// waiting. These are deployment mistakes, not outages, so callers that
1971+ /// retry should give up instead of looping forever.
1972+ pub fn check_config ( & self ) -> Result < ( ) > {
1973+ if self . keys . gateway_app_id . is_empty ( ) {
1974+ bail ! ( "Missing allowed dstack-gateway app id" ) ;
1975+ }
1976+ if self . shared . sys_config . gateway_urls . is_empty ( ) {
1977+ bail ! ( "Missing gateway urls" ) ;
1978+ }
1979+ Ok ( ( ) )
1980+ }
1981+
1982+ /// Register with dstack-gateway and apply the returned WireGuard config.
1983+ pub async fn refresh ( & self , force : bool ) -> Result < ( ) > {
1984+ GatewayContext :: new ( & self . shared , & self . keys )
1985+ . setup ( force)
1986+ . await
1987+ }
1988+ }
1989+
1990+ pub async fn cmd_gateway_refresh ( args : GatewayRefreshArgs ) -> Result < ( ) > {
1991+ GatewayRefresher :: load ( & args. work_dir ) ?
1992+ . refresh ( args. force )
1993+ . await
19471994}
19481995
19491996struct AppIdValidator {
@@ -2847,9 +2894,27 @@ impl Stage1<'_> {
28472894 self . vmm
28482895 . notify_q ( "boot.progress" , "setting up dstack-gateway" )
28492896 . await ;
2850- GatewayContext :: new ( & self . shared , & self . keys )
2897+ if let Err ( error ) = GatewayContext :: new ( & self . shared , & self . keys )
28512898 . setup ( true )
2852- . await ?;
2899+ . await
2900+ {
2901+ warn ! (
2902+ "dstack-gateway registration is unavailable during boot; continuing without a route: {error:#}"
2903+ ) ;
2904+ // Boot no longer fails here, so a guest log line would be the only
2905+ // trace of it: the VM would report a clean boot while having no
2906+ // ingress at all. Report it to the host so the degraded state is
2907+ // visible from the VMM. The gateway checker clears this once it
2908+ // manages to register.
2909+ self . vmm
2910+ . notify_q (
2911+ "boot.error" ,
2912+ & format ! (
2913+ "dstack-gateway registration failed, the app has no ingress route: {error:#}"
2914+ ) ,
2915+ )
2916+ . await ;
2917+ }
28532918 self . vmm
28542919 . notify_q ( "boot.progress" , "setting up docker" )
28552920 . await ;
0 commit comments