inital working commit

This commit is contained in:
Tamipes 2025-11-24 23:02:02 +01:00
commit 28109122b2
18 changed files with 3321 additions and 0 deletions

292
src/main.rs Normal file
View file

@ -0,0 +1,292 @@
//! This is a simple imitation of the basic functionality of kubectl:
//! kubectl {get, delete, apply, watch, edit} <resource> [name]
//! with labels and namespace selectors supported.
use anyhow::{bail, Context, Result};
use futures::{StreamExt, TryStreamExt};
use k8s_openapi::{
api::apps::v1::Deployment, apimachinery::pkg::apis::meta::v1::Time, chrono::Utc,
};
use kube::{
api::{Api, DynamicObject, ListParams, Patch, PatchParams, ResourceExt},
core::GroupVersionKind,
discovery::{ApiCapabilities, ApiResource, Discovery, Scope},
runtime::{
wait::{await_condition, conditions::is_deleted},
watcher, WatchStreamExt,
},
Client,
};
use tracing::*;
mod packets;
mod types;
#[derive(clap::Parser)]
struct App {
#[arg(long, short, default_value_t = OutputMode::Pretty)]
output: OutputMode,
#[arg(long, short)]
file: Option<std::path::PathBuf>,
#[arg(long, short = 'l')]
selector: Option<String>,
#[arg(long, short)]
namespace: Option<String>,
#[arg(long, short = 'A')]
all: bool,
verb: Verb,
resource: Option<String>,
name: Option<String>,
}
#[derive(Clone, PartialEq, Eq, clap::ValueEnum)]
enum OutputMode {
Pretty,
Yaml,
}
impl OutputMode {
fn as_str(&self) -> &'static str {
match self {
Self::Pretty => "pretty",
Self::Yaml => "yaml",
}
}
}
impl std::fmt::Display for OutputMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.pad(self.as_str())
}
}
#[derive(Clone, PartialEq, Eq, Debug, clap::ValueEnum)]
enum Verb {
Get,
Delete,
Edit,
Watch,
Apply,
}
fn resolve_api_resource(
discovery: &Discovery,
name: &str,
) -> Option<(ApiResource, ApiCapabilities)> {
// iterate through groups to find matching kind/plural names at recommended versions
// and then take the minimal match by group.name (equivalent to sorting groups by group.name).
// this is equivalent to kubectl's api group preference
discovery
.groups()
.flat_map(|group| {
group
.resources_by_stability()
.into_iter()
.map(move |res| (group, res))
})
.filter(|(_, (res, _))| {
// match on both resource name and kind name
// ideally we should allow shortname matches as well
name.eq_ignore_ascii_case(&res.kind) || name.eq_ignore_ascii_case(&res.plural)
})
.min_by_key(|(group, _res)| group.name())
.map(|(_, res)| res)
}
impl App {
async fn get(&self, api: Api<DynamicObject>, lp: ListParams) -> Result<()> {
let mut result: Vec<_> = if let Some(n) = &self.name {
vec![api.get(n).await?]
} else {
api.list(&lp).await?.items
};
result
.iter_mut()
.for_each(|x| x.managed_fields_mut().clear()); // hide managed fields
match self.output {
OutputMode::Yaml => println!("{}", serde_yaml::to_string(&result)?),
OutputMode::Pretty => {
// Display style; size columns according to longest name
let max_name = result
.iter()
.map(|x| x.name_any().len() + 2)
.max()
.unwrap_or(63);
println!("{0:<width$} {1:<20}", "NAME", "AGE", width = max_name);
for inst in result {
let age = inst
.creation_timestamp()
.map(format_creation)
.unwrap_or_default();
println!(
"{0:<width$} {1:<20}",
inst.name_any(),
age,
width = max_name
);
}
}
}
Ok(())
}
async fn delete(&self, api: Api<DynamicObject>, lp: ListParams) -> Result<()> {
if let Some(n) = &self.name {
if let either::Either::Left(pdel) = api.delete(n, &Default::default()).await? {
// await delete before returning
await_condition(api, n, is_deleted(&pdel.uid().unwrap())).await?;
}
} else {
api.delete_collection(&Default::default(), &lp).await?;
}
Ok(())
}
async fn watch(&self, api: Api<DynamicObject>, mut wc: watcher::Config) -> Result<()> {
if let Some(n) = &self.name {
wc = wc.fields(&format!("metadata.name={n}"));
}
// present a dumb table for it for now. kubectl does not do this anymore.
let mut stream = watcher(api, wc).applied_objects().boxed();
println!("{0:<width$} {1:<20}", "NAME", "AGE", width = 63);
while let Some(inst) = stream.try_next().await? {
let age = inst
.creation_timestamp()
.map(format_creation)
.unwrap_or_default();
println!("{0:<width$} {1:<20}", inst.name_any(), age, width = 63);
}
Ok(())
}
async fn edit(&self, api: Api<DynamicObject>) -> Result<()> {
if let Some(n) = &self.name {
let mut orig = api.get(n).await?;
orig.managed_fields_mut().clear(); // hide managed fields
let input = serde_yaml::to_string(&orig)?;
debug!("opening {} in {:?}", orig.name_any(), edit::get_editor());
let edited = edit::edit(&input)?;
if edited != input {
info!("updating changed object {}", orig.name_any());
let data: DynamicObject = serde_yaml::from_str(&edited)?;
// NB: simplified kubectl constructs a merge-patch of differences
api.replace(n, &Default::default(), &data).await?;
}
} else {
warn!("need a name to edit");
}
Ok(())
}
async fn apply(&self, client: Client, discovery: &Discovery) -> Result<()> {
let ssapply = PatchParams::apply("kubectl-light").force();
let pth = self.file.clone().expect("apply needs a -f file supplied");
let yaml = std::fs::read_to_string(&pth)
.with_context(|| format!("Failed to read {}", pth.display()))?;
for doc in multidoc_deserialize(&yaml)? {
let obj: DynamicObject = serde_yaml::from_value(doc)?;
let namespace = obj
.metadata
.namespace
.as_deref()
.or(self.namespace.as_deref());
let gvk = if let Some(tm) = &obj.types {
GroupVersionKind::try_from(tm)?
} else {
bail!("cannot apply object without valid TypeMeta {:?}", obj);
};
let name = obj.name_any();
if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) {
let api = dynamic_api(ar, caps, client.clone(), namespace, false);
trace!("Applying {}: \n{}", gvk.kind, serde_yaml::to_string(&obj)?);
let data: serde_json::Value = serde_json::to_value(&obj)?;
let _r = api.patch(&name, &ssapply, &Patch::Apply(data)).await?;
info!("applied {} {}", gvk.kind, name);
} else {
warn!("Cannot apply document for unknown {:?}", gvk);
}
}
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
// let app: App = clap::Parser::parse();
let kubeconfig = kube::config::Kubeconfig::read()?;
let client = Client::try_from(kubeconfig)?;
let deployments: Api<Deployment> = Api::default_namespaced(client);
let lp: ListParams = ListParams::default();
println!("{:?}", deployments.list(&lp).await?);
// // discovery (to be able to infer apis from kind/plural only)
// let discovery = Discovery::new(client.clone()).run().await?;
// // Defer to methods for verbs
// if let Some(resource) = &app.resource {
// // Common discovery, parameters, and api configuration for a single resource
// let (ar, caps) = resolve_api_resource(&discovery, resource)
// .with_context(|| format!("resource {resource:?} not found in cluster"))?;
// let mut lp = ListParams::default();
// if let Some(label) = &app.selector {
// lp = lp.labels(label);
// }
// let mut wc = watcher::Config::default();
// if let Some(label) = &app.selector {
// wc = wc.labels(label);
// }
// let api = dynamic_api(ar, caps, client, app.namespace.as_deref(), app.all);
// tracing::info!(?app.verb, ?resource, name = ?app.name.clone().unwrap_or_default(), "requested objects");
// match app.verb {
// Verb::Edit => app.edit(api).await?,
// Verb::Get => app.get(api, lp).await?,
// Verb::Delete => app.delete(api, lp).await?,
// Verb::Watch => app.watch(api, wc).await?,
// Verb::Apply => bail!("verb {:?} cannot act on an explicit resource", app.verb),
// }
// } else if app.verb == Verb::Apply {
// app.apply(client, &discovery).await? // multi-resource special behaviour
// }
Ok(())
}
fn dynamic_api(
ar: ApiResource,
caps: ApiCapabilities,
client: Client,
ns: Option<&str>,
all: bool,
) -> Api<DynamicObject> {
if caps.scope == Scope::Cluster || all {
Api::all_with(client, &ar)
} else if let Some(namespace) = ns {
Api::namespaced_with(client, namespace, &ar)
} else {
Api::default_namespaced_with(client, &ar)
}
}
fn format_creation(time: Time) -> String {
let dur = Utc::now().signed_duration_since(time.0);
match (dur.num_days(), dur.num_hours(), dur.num_minutes()) {
(days, _, _) if days > 0 => format!("{days}d"),
(_, hours, _) if hours > 0 => format!("{hours}h"),
(_, _, mins) => format!("{mins}m"),
}
}
pub fn multidoc_deserialize(data: &str) -> Result<Vec<serde_yaml::Value>> {
use serde::Deserialize;
let mut docs = vec![];
for de in serde_yaml::Deserializer::from_str(data) {
docs.push(serde_yaml::Value::deserialize(de)?);
}
Ok(docs)
}

View file

@ -0,0 +1,41 @@
use std::io::Write;
use crate::{
packets::{Packet, SendPacket},
types::VarString,
};
/// id: 0x00
#[derive(Debug)]
pub struct Disconnect {
reason: VarString,
all: Vec<u8>,
}
impl Disconnect {
pub fn parse(packet: Packet) -> Option<Disconnect> {
let mut reader = packet.data.into_iter();
Some(Disconnect {
all: packet.all,
reason: VarString::parse(&mut reader)?,
})
}
pub fn get_string(&self) -> String {
self.reason.get_value()
}
pub fn set_reason(reason: String) -> Option<Disconnect> {
let vec = VarString::from(reason).move_data()?;
Disconnect::parse(Packet::from_bytes(0, vec)?)
}
pub fn get_all(&self) -> Vec<u8> {
self.all.clone()
}
}
impl SendPacket for Disconnect {
fn send_packet(&self, stream: &mut std::net::TcpStream) -> std::io::Result<()> {
stream.write_all(&self.all)?;
stream.flush()?;
Ok(())
}
}

View file

@ -0,0 +1,2 @@
pub mod login;
pub mod status;

View file

@ -0,0 +1,155 @@
use std::{collections::HashMap, io::Write};
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
packets::{Packet, SendPacket},
types::VarString,
};
pub trait StatusTrait {
fn get_players_online(&self) -> i32;
fn set_description(&mut self, str: String);
fn get_description(&mut self) -> &mut String;
fn get_string(&self) -> String;
}
impl StatusTrait for StatusStructNew {
fn get_players_online(&self) -> i32
where
Self: Sized,
{
self.players.online
}
fn set_description(&mut self, str: String)
where
Self: Sized,
{
self.description.text = str;
}
fn get_description(&mut self) -> &mut String
where
Self: Sized,
{
&mut self.description.text
}
fn get_string(&self) -> String {
serde_json::to_string(&self).unwrap()
}
}
impl StatusTrait for StatusStructOld {
fn get_players_online(&self) -> i32 {
self.players.online
}
fn set_description(&mut self, str: String) {
self.description = str;
}
fn get_description(&mut self) -> &mut String {
&mut self.description
}
fn get_string(&self) -> String {
serde_json::to_string(&self).unwrap()
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct StatusStructNew {
pub version: StatusVersion,
pub enforcesSecureChat: Option<bool>,
pub description: StatusDescription,
pub players: StatusPlayers,
#[serde(flatten)]
extra: HashMap<String, Value>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct StatusStructOld {
pub version: StatusVersion,
pub description: String,
pub players: StatusPlayers,
#[serde(flatten)]
extra: HashMap<String, Value>,
}
impl StatusStructNew {
pub fn create() -> StatusStructNew {
StatusStructNew {
version: StatusVersion {
name: "???".to_owned(),
protocol: -1,
},
enforcesSecureChat: Some(false),
description: StatusDescription {
text: "Proxy default config".to_owned(),
},
players: StatusPlayers { max: 0, online: 0 },
extra: HashMap::new(),
}
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct StatusDescription {
pub text: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct StatusVersion {
pub name: String,
pub protocol: i32,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct StatusPlayers {
pub max: i32,
pub online: i32,
}
/// id: 0x00
#[derive(Debug)]
pub struct StatusResponse {
json: VarString,
all: Vec<u8>,
}
impl StatusResponse {
pub fn parse(packet: Packet) -> Option<StatusResponse> {
let mut reader = packet.data.into_iter();
Some(StatusResponse {
all: packet.all,
json: VarString::parse(&mut reader)?,
})
}
pub fn get_string(&self) -> String {
self.json.get_value()
}
pub fn get_json(&self) -> Option<Box<dyn StatusTrait>> {
if let Some(json) = serde_json::from_str::<StatusStructNew>(&self.json.get_value()).ok() {
return Some(Box::new(json));
} else if let Some(json) =
serde_json::from_str::<StatusStructOld>(&self.json.get_value()).ok()
{
return Some(Box::new(json));
}
None
}
pub fn set_json(json: Box<dyn StatusTrait>) -> StatusResponse {
let vec = VarString::from(json.get_string()).move_data().unwrap();
StatusResponse::parse(Packet::from_bytes(0, vec).unwrap()).unwrap()
}
pub fn get_all(&self) -> Vec<u8> {
self.all.clone()
}
}
impl SendPacket for StatusResponse {
fn send_packet(&self, stream: &mut std::net::TcpStream) -> std::io::Result<()> {
stream.write_all(&self.all)?;
stream.flush()?;
Ok(())
}
}

120
src/packets/mod.rs Normal file
View file

@ -0,0 +1,120 @@
use mc_proxy::{types::*, ProtocolState};
use std::{
io::{self, Read, Write},
net::TcpStream,
};
pub mod clientbound;
pub mod serverbound;
#[derive(Debug)]
pub struct Packet {
pub id: VarInt,
length: VarInt,
pub data: Vec<u8>,
pub all: Vec<u8>,
}
pub trait SendPacket {
fn send_packet(&self, stream: &mut TcpStream) -> io::Result<()>;
}
impl SendPacket for Packet {
fn send_packet(&self, stream: &mut TcpStream) -> io::Result<()> {
stream.write_all(&self.all)?;
Ok(())
}
}
impl Packet {
pub fn from_bytes(id: i32, data: Vec<u8>) -> Option<Packet> {
let id = VarInt::from(id)?;
let length = VarInt::from((data.len() + id.get_data().len()) as i32)?;
let mut all = length.get_data();
all.append(&mut id.get_data());
all.append(&mut data.clone());
Some(Packet {
id,
length,
data,
all,
})
}
pub fn new(id: i32, data: Vec<u8>) -> Option<Packet> {
let mut vec = VarInt::from(id)?.get_data();
vec.append(&mut data.clone());
let mut all = VarInt::from(vec.len() as i32)?.get_data();
all.append(&mut vec.clone());
all.append(&mut data.clone());
Some(Packet {
id: VarInt::from(id)?,
length: VarInt::from(vec.len() as i32)?,
data,
all,
})
}
pub fn parse(buf: &mut TcpStream) -> Option<Packet> {
let bytes_iter = &mut buf.bytes().into_iter().map(|x| x.unwrap());
let length = VarInt::parse(bytes_iter)?;
// println!("---length: {length}");
let id = match VarInt::parse(bytes_iter) {
Some(x) => x,
None => {
println!("Packet id problem(it was None)! REEEEEEEEEEEEEEEEEEEE");
panic!();
// return None;
}
};
// println!("---id: {id}");
if id.get_int() == 122 {
return None;
}
let mut data: Vec<u8> = vec![0; length.get_int() as usize - id.get_data().len()];
match buf.read_exact(&mut data) {
Ok(_) => {
// data_id.append(&mut data.clone());
// data_length.append(&mut data_id);
let mut vec = id.get_data();
vec.append(&mut data.clone());
let mut all = length.get_data();
all.append(&mut vec);
Some(Packet {
id,
length,
data,
all,
})
}
Err(x) => {
if id.get_int() == 122 {
return None;
} else {
println!("len = {}: {:?}", length.get_int(), length.get_data());
println!("Buffer read error: {x}");
return None;
}
}
}
}
pub fn all(&self) -> Option<Vec<u8>> {
let mut vec = self.id.get_data();
vec.append(&mut self.data.clone());
let mut all = VarInt::from(vec.len() as i32)?.get_data();
all.append(&mut vec);
return Some(all);
}
pub fn proto_name(&self, state: &ProtocolState) -> String {
match state {
ProtocolState::Handshaking => match self.id.get_int() {
0 => "Handshake".to_owned(),
_ => "error".to_owned(),
},
ProtocolState::Status => match self.id.get_int() {
0 => "StatusRequest".to_owned(),
1 => "PingRequest".to_owned(),
_ => "error".to_owned(),
},
_ => "Dont care state".to_owned(),
}
}
}

View file

@ -0,0 +1,70 @@
use std::io::Write;
use nix::NixPath;
use crate::{
packets::{Packet, SendPacket},
types::{UShort, VarInt, VarString},
};
/// id: 0x00
pub struct Handshake {
pub protocol_version: VarInt,
pub server_address: VarString,
pub server_port: UShort,
pub next_state: VarInt,
all: Vec<u8>,
}
impl Handshake {
pub fn parse(packet: Packet) -> Option<Handshake> {
let mut reader = packet.data.clone().into_iter();
let protocol_version = VarInt::parse(&mut reader)?;
let server_address = VarString::parse(&mut reader)?;
let server_port = UShort::parse(&mut reader)?;
let next_state = VarInt::parse(&mut reader)?;
Some(Handshake {
protocol_version,
server_address,
server_port,
next_state,
all: packet.all,
})
}
pub fn get_server_address(&self) -> String {
self.server_address.get_value()
}
pub fn get_next_state(&self) -> i32 {
self.next_state.get_int()
}
pub fn create(
protocol_version: VarInt,
server_address: VarString,
server_port: UShort,
next_state: VarInt,
) -> Option<Handshake> {
let mut vec = VarInt::from(0)?.get_data();
vec.append(&mut protocol_version.get_data());
vec.append(&mut server_address.get_data()?);
vec.append(&mut server_port.get_data());
vec.append(&mut next_state.get_data());
let mut all = VarInt::from(vec.len() as i32)?.get_data();
all.append(&mut vec);
Some(Handshake {
protocol_version,
server_address,
server_port,
next_state,
all,
})
}
}
impl SendPacket for Handshake {
fn send_packet(&self, stream: &mut std::net::TcpStream) -> std::io::Result<()> {
stream.write_all(&self.all)?;
stream.flush()?;
Ok(())
}
}

View file

@ -0,0 +1,2 @@
pub mod handshake;
pub mod status;

View file

@ -0,0 +1,22 @@
use std::io::Write;
use crate::packets::{Packet, SendPacket};
/// id: 0x00
pub struct StatusRequest {
all: Vec<u8>,
}
impl StatusRequest {
pub fn parse(packet: Packet) -> Option<StatusRequest> {
Some(StatusRequest { all: packet.all })
}
}
impl SendPacket for StatusRequest {
fn send_packet(&self, stream: &mut std::net::TcpStream) -> std::io::Result<()> {
stream.write_all(&self.all)?;
stream.flush()?;
Ok(())
}
}

172
src/types/mod.rs Normal file
View file

@ -0,0 +1,172 @@
use std::fmt::Display;
const SEGMENT_BITS: u8 = 0x7F;
const CONTINUE_BIT: u8 = 0x80;
#[derive(Debug)]
pub struct VarInt {
value: i32,
data: Vec<u8>,
}
impl Display for VarInt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
impl VarInt {
pub fn get_int(&self) -> i32 {
self.value
}
/// Clones the data for use, the sturct is still usable.
pub fn get_data(&self) -> Vec<u8> {
self.data.clone()
}
/// Moves the data out from the struct. Struct is useless later.
pub fn move_data(self) -> Vec<u8> {
self.data
}
pub fn read<I>(data: &mut I) -> Option<i32>
where
I: Iterator<Item = u8>,
{
let mut value: i32 = 0;
let mut position = 0;
for current_byte in data {
value |= ((current_byte & SEGMENT_BITS) as i32) << position;
if current_byte & CONTINUE_BIT == 0 {
break;
}
position += 7;
if position > 32 {
todo!();
}
}
Some(value)
}
pub fn parse<I>(reader: &mut I) -> Option<VarInt>
where
I: Iterator<Item = u8>,
{
let mut value: i32 = 0;
let mut position = 0;
let mut vec = Vec::new();
for current_byte in reader {
let current_byte = current_byte;
vec.push(current_byte);
value |= ((current_byte & SEGMENT_BITS) as i32) << position;
if current_byte & CONTINUE_BIT == 0 {
break;
}
position += 7;
if position > 32 {
return None;
}
}
Some(VarInt { value, data: vec })
}
pub fn from(num: i32) -> Option<VarInt> {
Some(VarInt {
value: num,
data: VarInt::write_varint(num)?,
})
}
fn write_varint(num: i32) -> Option<Vec<u8>> {
let mut num = num;
let mut vec = Vec::new();
if num == 0 {
vec.push(0);
}
while num != 0 {
vec.push(num as u8 & SEGMENT_BITS);
num = num >> 7;
if num != 0 {
let a = vec.pop()?;
vec.push(a | CONTINUE_BIT);
}
}
Some(vec)
}
}
#[derive(Debug)]
pub struct VarString {
value: String,
}
impl VarString {
pub fn get_value(&self) -> String {
self.value.clone()
}
pub fn move_data(self) -> Option<Vec<u8>> {
let mut vec = VarInt::from(self.value.len() as i32)?.move_data();
vec.append(&mut (Vec::from(self.value.as_bytes())));
Some(vec)
}
pub fn get_data(&self) -> Option<Vec<u8>> {
let mut vec = VarInt::from(self.value.len() as i32)?.move_data();
vec.append(&mut (Vec::from(self.value.as_bytes())));
Some(vec)
}
pub fn from(string: String) -> VarString {
VarString { value: string }
}
pub fn parse<I>(data: &mut I) -> Option<VarString>
where
I: Iterator<Item = u8>,
{
let length = VarInt::read(data)?;
let mut vec = Vec::new();
for _ in 0..length {
vec.push(data.next()?);
}
Some(VarString {
value: String::from_utf8(vec).ok()?,
})
}
}
pub struct UShort {
value: u16,
data: Vec<u8>,
}
impl UShort {
pub fn get_value(&self) -> u16 {
self.value
}
pub fn get_data(&self) -> Vec<u8> {
self.data.clone()
}
pub fn parse<I>(data: &mut I) -> Option<UShort>
where
I: Iterator<Item = u8>,
{
let mut vec = vec![data.next()?];
let mut int: u16 = vec[0] as u16;
int = int << 8;
vec.push(data.next()?);
int |= vec[1] as u16;
Some(UShort {
value: int,
data: vec,
})
}
pub fn from(short: u16) -> UShort {
let mut vec = vec![(short >> 8) as u8];
vec.push(((short >> 8) << 8) as u8);
UShort {
value: short,
data: vec,
}
}
}