Commit Diff


commit - 8b43cacff071605d7df7f2c9e7af81a0c78d36aa
commit + 5dd316e79476797a698d46a2e5699033134176a9
blob - d41a3be9a1ce49b0400358313e781c365b7c4a6e
blob + cddf32fda8c3b4f2ffaa843912bcca4858945bf2
--- Cargo.lock
+++ Cargo.lock
@@ -643,6 +643,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
 name = "cfg-if"
 version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -718,6 +724,16 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
 
 [[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
 name = "concurrent-queue"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1667,6 +1683,28 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 
 [[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
 name = "jobserver"
 version = "0.1.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1830,12 +1868,16 @@ name = "kopsctl"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "aws-config",
+ "aws-types",
  "clap",
  "dialoguer",
+ "kops_aws_sso",
  "kops_log",
  "kops_protocol",
  "tokio",
  "tracing",
+ "webbrowser",
 ]
 
 [[package]]
@@ -1843,6 +1885,7 @@ name = "kopsd"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "chrono",
  "clap",
  "config",
  "daemonize",
@@ -2085,6 +2128,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
 name = "nu-ansi-term"
 version = "0.50.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2118,6 +2167,31 @@ dependencies = [
 ]
 
 [[package]]
+name = "objc2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags",
+ "objc2",
+]
+
+[[package]]
 name = "once_cell"
 version = "1.21.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2543,6 +2617,15 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 
 [[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
 name = "schannel"
 version = "0.1.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3308,6 +3391,16 @@ source = "registry+https://github.com/rust-lang/crates
 checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
 
 [[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
 name = "want"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3377,6 +3470,41 @@ dependencies = [
 ]
 
 [[package]]
+name = "web-sys"
+version = "0.3.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webbrowser"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00f1243ef785213e3a32fa0396093424a3a6ea566f9948497e5a2309261a4c97"
+dependencies = [
+ "core-foundation 0.10.1",
+ "jni",
+ "log",
+ "ndk-context",
+ "objc2",
+ "objc2-foundation",
+ "url",
+ "web-sys",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "windows-core"
 version = "0.62.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3443,6 +3571,15 @@ dependencies = [
 
 [[package]]
 name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
@@ -3470,6 +3607,21 @@ dependencies = [
 
 [[package]]
 name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
@@ -3503,6 +3655,12 @@ dependencies = [
 
 [[package]]
 name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
@@ -3515,6 +3673,12 @@ checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cea
 
 [[package]]
 name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
@@ -3527,6 +3691,12 @@ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b
 
 [[package]]
 name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
@@ -3551,6 +3721,12 @@ checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559ae
 
 [[package]]
 name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
@@ -3563,6 +3739,12 @@ checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c8925
 
 [[package]]
 name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
@@ -3575,6 +3757,12 @@ checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29
 
 [[package]]
 name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
@@ -3587,6 +3775,12 @@ checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536
 
 [[package]]
 name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
blob - 9e54c2456c150d9dfe7a305a792fe99c4975219b
blob + f62f88dfbbf876b7f19291bc78d6c856a115d1f8
--- Cargo.toml
+++ Cargo.toml
@@ -28,6 +28,7 @@ unused_unsafe = "warn"
 all = { level = "warn", priority = -1 }
 
 [workspace.dependencies]
+kops_aws_sso = { version = "=0.1.0", path = "crates/kops_aws_sso" }
 kops_log = { version = "=0.1.0", path = "crates/kops_log" }
 kops_protocol = { version = "=0.1.0", path = "crates/kops_protocol" }
 
@@ -36,6 +37,7 @@ aws-config = { version = "=1.8.11", features = ["behav
 aws-credential-types = "=1.2.10"
 aws-sdk-sso = "=1.90.0"
 aws-sdk-ssooidc = "=1.92.0"
+aws-types = "=1.3.10"
 bincode = "=2.0.1"
 chrono = { version = "0.4", features = ["clock", "serde"] }
 clap = { version = "=4.5.53", features = ["derive", "env"] }
blob - 2c19e0b681afb8959610e668a41f2be2e5f590bb
blob + f56d5d4bcc487b5ef9a0ab4f3df36a1fd084876b
--- crates/kops_aws_sso/src/lib.rs
+++ crates/kops_aws_sso/src/lib.rs
@@ -87,6 +87,10 @@ where
         .await
         .context("failed to start device authorization")?;
 
+    let verification_uri = device_auth
+        .verification_uri_complete()
+        .ok_or(anyhow!("missing verification URI"))?;
+
     let device_code = must(device_auth.device_code(), "device_code")?;
     let verification_uri = device_auth
         .verification_uri_complete()
blob - 67012d53d2e0050150e27f10dbbbb755ee53f201
blob + e366e724d0780028563ba609e69247912d6eb450
--- crates/kops_protocol/src/lib.rs
+++ crates/kops_protocol/src/lib.rs
@@ -27,7 +27,7 @@ pub enum Request {
     /// Health-check: the daemon must reply with `Response::Pong`.
     Ping,
 
-    Login,
+    Login(LoginRequest),
 
     Pods(PodsRequest),
     Env(EnvRequest),
@@ -42,6 +42,7 @@ pub enum Response {
     /// Response for `Request::Ping`,
     Pong,
 
+    /// Login successfully registered in daemon
     LoginOk,
 
     Version(VersionInfo),
@@ -164,3 +165,30 @@ fn extract_status_fields(
 
     (reason, message, ready, restarts)
 }
+
+#[derive(Debug, Encode, Decode)]
+pub struct LoginRequest {
+    /// Logical profile name, e.g. "dev" or "prod".
+    pub name: String,
+
+    /// Optional region associated with this profile.
+    pub region: Option<String>,
+
+    /// AWS account ID where this role is valid.
+    pub account_id: String,
+
+    /// IAM role name assumed via SSO.
+    pub role_name: String,
+
+    /// AWS access key ID from SSO.
+    pub access_key_id: String,
+
+    /// AWS secret access key from SSO.
+    pub secret_access_key: String,
+
+    /// Temporary session token from SSO.
+    pub session_token: String,
+
+    /// Expiration of this session as Unix epoch milliseconds (UTC).
+    pub expires_at_epoch_ms: i64,
+}
blob - f681f50610a0ae1ffb26827e7357f7f1914c68c2
blob + c04aedf8fc4b9e16e60484aa925f646a89eafda1
--- kopsctl/Cargo.toml
+++ kopsctl/Cargo.toml
@@ -14,12 +14,16 @@ description.workspace = true
 
 [dependencies]
 anyhow.workspace = true
+aws-config.workspace = true
+aws-types.workspace = true
 clap.workspace = true
 dialoguer.workspace = true
+kops_aws_sso.workspace = true
 kops_log.workspace = true
 kops_protocol.workspace = true
 tokio.workspace = true
 tracing.workspace = true
+webbrowser.workspace = true
 
 [lints]
 workspace = true
blob - 1d33f4409dbe65886df7591f74bef53c64dbaa1c
blob + 1fdf4a4d1689ac189eb9d0cfca5ad5f7ae3f6817
--- kopsctl/src/cmd/login.rs
+++ kopsctl/src/cmd/login.rs
@@ -14,24 +14,115 @@
 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 //
 
-use anyhow::{Result, bail};
+use anyhow::{Result, anyhow, bail};
+use aws_config::BehaviorVersion;
+use aws_types::region::Region;
+use kops_aws_sso::{SsoLoginConfig, login_device_flow};
+use kops_protocol::{LoginRequest, Request, Response};
 
-use kops_protocol::{Request, Response};
-
 use crate::helper::send_request;
 
-pub async fn execute() -> Result<()> {
-    let resp = send_request(Request::Login).await?;
+pub async fn execute(name: String, region: Option<String>) -> Result<()> {
+    let region = region
+        .or_else(|| std::env::var("AWS_REGION").ok())
+        .unwrap_or_else(|| "us-east-1".to_string());
 
+    let start_url = std::env::var("KOPS_SSO_START_URL")
+        .map_err(|_| anyhow!("KOPS_SSO_START_URL not set"))?;
+    let account_id = std::env::var("KOPS_SSO_ACCOUNT_ID")
+        .map_err(|_| anyhow!("KOPS_SSO_ACCOUNT_ID not set"))?;
+    let role_name = std::env::var("KOPS_SSO_ROLE_NAME")
+        .map_err(|_| anyhow!("KOPS_SSO_ROLE_NAME not set"))?;
+
+    let client_name = format!("kops");
+
+    let sso_cfg = SsoLoginConfig {
+        region: region.clone(),
+        start_url,
+        account_id: account_id.clone(),
+        role_name: role_name.clone(),
+        client_name,
+    };
+
+    let sdk_config = aws_config::from_env()
+        .region(Region::new(region.clone()))
+        .load()
+        .await;
+
+    println!("Starting AWS SSO device flow for profile '{name}'...");
+    println!("Region     : {region}");
+    println!("Account ID : {account_id}");
+    println!("Role name  : {role_name}");
+    println!();
+
+    let session = login_device_flow(&sdk_config, &sso_cfg, |info| {
+        println!("SSO user code       : {}", info.user_code);
+        println!("Verification URL    : {}", info.verification_uri);
+
+        if let Some(full) = &info.verification_uri_complete {
+            println!("Verification (full) : {full}");
+
+            if let Err(err) = webbrowser::open(full) {
+                eprintln!("Failed to open browser automatically: {err}");
+                eprintln!("Please open the URL manually.");
+            } else {
+                println!(
+                    "Browser opened automatically, please finish authentication."
+                );
+            }
+        } else if let Err(err) = webbrowser::open(&info.verification_uri) {
+            eprintln!("Failed to open browser automatically: {err}");
+            eprintln!("Please open the URL manually.");
+        } else {
+            println!(
+                "Browser opened automatically, please finish authentication."
+            );
+        }
+
+        println!();
+        println!("Waiting for AWS SSO authorization...");
+    })
+    .await?;
+
+    println!(
+        "Successfully obtained AWS credentials for account {} role {}",
+        session.account_id, session.role_name
+    );
+
+    let expires_at_epoch_ms = session.expires_at.timestamp_millis();
+
+    let creds = session.credentials;
+    let access_key_id = creds.access_key_id().to_string();
+    let secret_access_key = creds.secret_access_key().to_string();
+    let session_token = creds
+        .session_token()
+        .ok_or_else(|| anyhow!("missing session token in AWS credentials"))?
+        .to_string();
+
+    let req = Request::Login(LoginRequest {
+        name: name.clone(),
+        region: Some(region),
+        account_id,
+        role_name,
+        access_key_id,
+        secret_access_key,
+        session_token,
+        expires_at_epoch_ms,
+    });
+
+    let resp = send_request(req).await?;
+
     match resp {
-        Response::LoginOk => print_login_info(),
-        Response::Error { message } => bail!("reponse error {message}"),
-        _ => bail!("unexpected response to version"),
+        Response::LoginOk => {
+            println!(
+                "kopsd registered AWS session for profile '{name}' successfully."
+            );
+        }
+        Response::Error { message } => {
+            bail!("daemon returned error on login: {message}");
+        }
+        _ => bail!("unexpected response to login"),
     }
 
     Ok(())
 }
-
-fn print_login_info() {
-    println!("login ok");
-}
blob - 8afcb435a3252d9616c2cdd15312e411aea7d5e4
blob + 165fa0600e9635c0a977f44d3806fcf0f044f431
--- kopsctl/src/main.rs
+++ kopsctl/src/main.rs
@@ -34,9 +34,17 @@ enum Command {
     /// Ping the daemon and expect a Pong response.
     Ping,
 
-    /// Login to the aws
-    Login,
+    /// Login via AWS SSO and register credentials in kopsd
+    Login {
+        /// Logical name for this credential set (e.g. dev, prod)
+        #[arg(long)]
+        name: String,
 
+        /// AWS region for SSO (optional, defaults to config or us-east-1)
+        #[arg(long)]
+        region: Option<String>,
+    },
+
     /// Show daemon and protocol version
     Version,
 
@@ -97,7 +105,9 @@ async fn main() -> Result<()> {
 
     match args.command {
         Command::Ping => cmd::ping::execute().await?,
-        Command::Login => cmd::login::execute().await?,
+        Command::Login { name, region } => {
+            cmd::login::execute(name, region).await?
+        }
         Command::Version => cmd::version::execute().await?,
         Command::Pods { cluster, namespace, failed_only } => {
             cmd::pods::execute(cluster, namespace, failed_only).await?
blob - b974932c59871600a915c99522dfb7e2eb3b2380
blob + d4f8871404773124280108e5f09e2bc43cb1e1e2
--- kopsd/Cargo.toml
+++ kopsd/Cargo.toml
@@ -14,6 +14,7 @@ description.workspace = true
 
 [dependencies]
 anyhow.workspace = true
+chrono.workspace = true
 clap.workspace = true
 config.workspace = true
 daemonize.workspace = true
blob - 804f99b9964ceb3f4a37a5da2acd641deab88d4d
blob + 27a3846d04bd7f1ecbc20b96792e43336afde4c2
--- kopsd/src/handler.rs
+++ kopsd/src/handler.rs
@@ -16,13 +16,16 @@
 
 use std::sync::Arc;
 
+use chrono::{TimeZone, Utc};
 use k8s_openapi::api::core::v1::Pod;
 use kops_protocol::{
-    EnvEntry, EnvRequest, PodSummary, PodsRequest, Request, Response,
+    EnvEntry, EnvRequest, LoginRequest, PodSummary, PodsRequest, Request,
+    Response,
 };
 use kube::ResourceExt;
+use tracing::info;
 
-use crate::state::DaemonState;
+use crate::state::{AwsSession, DaemonState};
 
 pub struct Handler {
     state: Arc<DaemonState>,
@@ -36,13 +39,46 @@ impl Handler {
     pub async fn handle(&self, req: Request) -> Response {
         match req {
             Request::Ping => Response::Pong,
-            Request::Login => Response::LoginOk,
+            Request::Login(login_req) => self.handle_login(login_req).await,
             Request::Version => self.handle_version().await,
             Request::Pods(p) => self.handle_pods(p).await,
             Request::Env(r) => self.handle_env(r).await,
         }
     }
 
+    async fn handle_login(&self, req: LoginRequest) -> Response {
+        info!(
+            "received AWS login for profile '{}' (account {} role {})",
+            req.name, req.account_id, req.role_name
+        );
+
+        let expires_at = Utc
+            .timestamp_millis_opt(req.expires_at_epoch_ms)
+            .single()
+            .unwrap_or_else(|| Utc::now());
+
+        let session = AwsSession {
+            account_id: req.account_id,
+            role_name: req.role_name,
+            region: req.region.clone(),
+            access_key_id: req.access_key_id,
+            secret_access_key: req.secret_access_key,
+            session_token: req.session_token,
+            expires_at,
+        };
+
+        match self.state.aws_sessions.lock() {
+            Ok(mut map) => {
+                map.insert(req.name.clone(), session);
+                info!("stored AWS session for profile '{}'", req.name);
+                Response::LoginOk
+            }
+            Err(_) => Response::Error {
+                message: "failed to lock aws_sessions map".into(),
+            },
+        }
+    }
+
     async fn handle_env(&self, req: EnvRequest) -> Response {
         let cluster = req
             .cluster
blob - 537caf4d8e725986ab270adea8b30f905bab7f49
blob + da25e6bbcbb16dbed6de3816f4e9803a375b845a
--- kopsd/src/server.rs
+++ kopsd/src/server.rs
@@ -63,11 +63,11 @@ fn run_fg(config: &KopsdConfig) -> Result<()> {
         .context("failed to build tokio runtime")?;
 
     rt.block_on(async move {
-        let mut clusters_map = std::collections::HashMap::new();
-        for c in &config.cluster {
-            let cs = init_cluster_state(c.clone()).await.unwrap();
-            clusters_map.insert(c.name.clone(), cs);
-        }
+        let clusters_map = std::collections::HashMap::new();
+        // for c in &config.cluster {
+        //     let cs = init_cluster_state(c.clone()).await.unwrap();
+        //     // clusters_map.insert(c.name.clone(), cs);
+        // }
 
         let default_cluster = config
             .kops
@@ -75,8 +75,15 @@ fn run_fg(config: &KopsdConfig) -> Result<()> {
             .clone()
             .unwrap_or_else(|| config.cluster[0].name.clone());
 
-        let state =
-            Arc::new(DaemonState { clusters: clusters_map, default_cluster });
+        // let state =
+        //     Arc::new(DaemonState { clusters: clusters_map, default_cluster });
+        let state = Arc::new(DaemonState {
+            clusters: clusters_map,
+            default_cluster,
+            aws_sessions: std::sync::Mutex::new(
+                std::collections::HashMap::new(),
+            ),
+        });
 
         // for c in config.cluster.clone() {
         //     let cluster_name = c.name.clone();
blob - e1fa0e1dd4458edfb8fd27f4644d3fe4367863b9
blob + 2bfcf0a77e38dfd27b14ab30f08961d9250275e7
--- kopsd/src/state.rs
+++ kopsd/src/state.rs
@@ -15,11 +15,24 @@
 //
 
 use std::collections::HashMap;
-use std::sync::Arc;
+use std::sync::{Arc, Mutex};
 
+use chrono::{DateTime, Utc};
 use k8s_openapi::api::core::v1::Pod;
 use kube::runtime::reflector::Store;
 
+/// AWS session stored in daemon memory.
+#[derive(Clone)]
+pub struct AwsSession {
+    pub account_id: String,
+    pub role_name: String,
+    pub region: Option<String>,
+    pub access_key_id: String,
+    pub secret_access_key: String,
+    pub session_token: String,
+    pub expires_at: DateTime<Utc>,
+}
+
 /// Logical name of the cluster (from config).
 pub type ClusterName = String;
 
@@ -27,12 +40,21 @@ pub type ClusterName = String;
 pub struct DaemonState {
     pub clusters: HashMap<ClusterName, Arc<ClusterState>>,
     pub default_cluster: ClusterName,
+
+    /// AWS sessions keyed by logical profile name ("dev", "prod", ...).
+    pub aws_sessions: Mutex<HashMap<String, AwsSession>>,
 }
 
 impl DaemonState {
     pub fn default_cluster(&self) -> &str {
         &self.default_cluster
     }
+
+    #[allow(dead_code)]
+    pub fn get_session(&self, name: &str) -> Option<AwsSession> {
+        let sessions = self.aws_sessions.lock().ok()?;
+        sessions.get(name).cloned()
+    }
 }
 
 /// Per-cluster in-memory state backed by a reflector Store.