diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0583782b4b1..8d9d7fdb3f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -570,3 +570,13 @@ cargo-zigbuild clippy --lib --bins --all-features --target x86_64-unknown-linux- If it doesn't work, try updating `cargo-zigbuild` (`cargo install cargo-zigbuild` or maybe `cargo install cargo-zigbuild --force`) or via `homebrew` if it was installed via homebrew. + +# Adding new target types + +Adding a new target type for mirrord requires changes in: + +1. `mirrord-config` crate - parsing the target from the user config; +2. `mirrord-cli` crate - verifying the user config; +3. `mirrord-kube` crate - resolving the target to the Kubernetes resource; +4. `mirrord-operator` crate - defining operator's `ClusterRole`; +5. `test` crate - testing `mirrord ls` command diff --git a/changelog.d/+ls-test.internal.md b/changelog.d/+ls-test.internal.md new file mode 100644 index 00000000000..f8c1eccecd5 --- /dev/null +++ b/changelog.d/+ls-test.internal.md @@ -0,0 +1 @@ +Updated `tests::operator::sanity::mirrord_ls` test after adding ReplicaSet support. diff --git a/changelog.d/+replicaset-target.added.md b/changelog.d/+replicaset-target.added.md new file mode 100644 index 00000000000..39f3a35347f --- /dev/null +++ b/changelog.d/+replicaset-target.added.md @@ -0,0 +1 @@ +Added Kubernetes ReplicaSet as a new target type. diff --git a/changelog.d/+set-agent-port.internal.md b/changelog.d/+set-agent-port.internal.md new file mode 100644 index 00000000000..6fc1f928401 --- /dev/null +++ b/changelog.d/+set-agent-port.internal.md @@ -0,0 +1 @@ +`mirrord-kube` now allows for setting agent listen port. diff --git a/changelog.d/+statfs-go-hook.removed.md b/changelog.d/+statfs-go-hook.removed.md new file mode 100644 index 00000000000..3c94b799360 --- /dev/null +++ b/changelog.d/+statfs-go-hook.removed.md @@ -0,0 +1 @@ +Removed faulty `statfs` hook for Go applications. diff --git a/mirrord-schema.json b/mirrord-schema.json index 1add0f98916..6a8bb2d6def 100644 --- a/mirrord-schema.json +++ b/mirrord-schema.json @@ -1669,6 +1669,24 @@ } ] }, + "ReplicaSetTarget": { + "type": "object", + "required": [ + "replica_set" + ], + "properties": { + "container": { + "type": [ + "string", + "null" + ] + }, + "replica_set": { + "type": "string" + } + }, + "additionalProperties": false + }, "RolloutTarget": { "description": " Mirror the rollout specified by [`RolloutTarget::rollout`].", "type": "object", @@ -1733,7 +1751,7 @@ "additionalProperties": false }, "Target": { - "description": " ## path\n\nSpecifies the running pod (or deployment) to mirror.\n\nSupports: - `targetless` - `pod/{pod-name}[/container/{container-name}]`; - `deployment/{deployment-name}[/container/{container-name}]`; - `rollout/{rollout-name}[/container/{container-name}]`; - `job/{job-name}[/container/{container-name}]`; - `cronjob/{cronjob-name}[/container/{container-name}]`; - `statefulset/{statefulset-name}[/container/{container-name}]`; - `service/{service-name}[/container/{container-name}]`;", + "description": " ## path\n\nSpecifies the running pod (or deployment) to mirror.\n\nSupports: - `targetless` - `pod/{pod-name}[/container/{container-name}]`; - `deployment/{deployment-name}[/container/{container-name}]`; - `rollout/{rollout-name}[/container/{container-name}]`; - `job/{job-name}[/container/{container-name}]`; - `cronjob/{cronjob-name}[/container/{container-name}]`; - `statefulset/{statefulset-name}[/container/{container-name}]`; - `service/{service-name}[/container/{container-name}]`; - `replicaset/{replicaset-name}[/container/{container-name}]`;", "anyOf": [ { "description": " [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/).", @@ -1791,6 +1809,14 @@ } ] }, + { + "description": " [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/).", + "allOf": [ + { + "$ref": "#/definitions/ReplicaSetTarget" + } + ] + }, { "description": " Spawn a new pod.", "type": "null" diff --git a/mirrord/cli/src/connection.rs b/mirrord/cli/src/connection.rs index dacec4fcb1b..fed2cc1b384 100644 --- a/mirrord/cli/src/connection.rs +++ b/mirrord/cli/src/connection.rs @@ -173,7 +173,7 @@ where let agent_connect_info = tokio::time::timeout( Duration::from_secs(config.agent.startup_timeout), - k8s_api.create_agent(progress, &config.target, Some(config), Default::default()), + k8s_api.create_agent(progress, &config.target, Some(config), None, None), ) .await .unwrap_or(Err(KubeApiError::AgentReadyTimeout)) diff --git a/mirrord/cli/src/verify_config.rs b/mirrord/cli/src/verify_config.rs index 4628118360b..c8b82aebcae 100644 --- a/mirrord/cli/src/verify_config.rs +++ b/mirrord/cli/src/verify_config.rs @@ -8,8 +8,8 @@ use mirrord_config::{ feature::FeatureConfig, target::{ cron_job::CronJobTarget, deployment::DeploymentTarget, job::JobTarget, pod::PodTarget, - rollout::RolloutTarget, service::ServiceTarget, stateful_set::StatefulSetTarget, Target, - TargetConfig, + replica_set::ReplicaSetTarget, rollout::RolloutTarget, service::ServiceTarget, + stateful_set::StatefulSetTarget, Target, TargetConfig, }, }; use serde::Serialize; @@ -47,6 +47,9 @@ enum VerifiedTarget { #[serde(untagged)] Service(ServiceTarget), + + #[serde(untagged)] + ReplicaSet(ReplicaSetTarget), } impl From for VerifiedTarget { @@ -59,6 +62,7 @@ impl From for VerifiedTarget { Target::CronJob(target) => Self::CronJob(target), Target::StatefulSet(target) => Self::StatefulSet(target), Target::Service(target) => Self::Service(target), + Target::ReplicaSet(target) => Self::ReplicaSet(target), Target::Targetless => Self::Targetless, } } @@ -75,6 +79,7 @@ impl From for TargetType { VerifiedTarget::CronJob(_) => TargetType::CronJob, VerifiedTarget::StatefulSet(_) => TargetType::StatefulSet, VerifiedTarget::Service(_) => TargetType::Service, + VerifiedTarget::ReplicaSet(_) => TargetType::ReplicaSet, } } } @@ -106,6 +111,7 @@ enum TargetType { CronJob, StatefulSet, Service, + ReplicaSet, } impl core::fmt::Display for TargetType { @@ -119,6 +125,7 @@ impl core::fmt::Display for TargetType { TargetType::CronJob => "cronjob", TargetType::StatefulSet => "statefulset", TargetType::Service => "service", + TargetType::ReplicaSet => "replicaset", }; f.write_str(stringifed) @@ -136,6 +143,7 @@ impl TargetType { Self::CronJob, Self::StatefulSet, Self::Service, + Self::ReplicaSet, ] .into_iter() } @@ -146,7 +154,7 @@ impl TargetType { Self::Pod => !(config.copy_target.enabled && config.copy_target.scale_down), Self::Job | Self::CronJob => config.copy_target.enabled, Self::Service => !config.copy_target.enabled, - Self::Deployment | Self::StatefulSet => true, + Self::Deployment | Self::StatefulSet | Self::ReplicaSet => true, } } } diff --git a/mirrord/config/configuration.md b/mirrord/config/configuration.md index 57b5cd32900..e1aa8368a35 100644 --- a/mirrord/config/configuration.md +++ b/mirrord/config/configuration.md @@ -1652,6 +1652,7 @@ Supports: - `statefulset/{statefulset-name}[/container/{container-name}]`; (requires mirrord Operator) - `service/{service-name}[/container/{container-name}]`; (requires mirrord Operator) +- `replicaset/{replicaset-name}[/container/{replicaset-name}]`; (requires mirrord Operator) ## telemetry {#root-telemetry} Controls whether or not mirrord sends telemetry data to MetalBear cloud. diff --git a/mirrord/config/src/target.rs b/mirrord/config/src/target.rs index d62e0ae1053..0ca815e0019 100644 --- a/mirrord/config/src/target.rs +++ b/mirrord/config/src/target.rs @@ -1,8 +1,8 @@ -use core::fmt; -use std::str::FromStr; +use std::{fmt, str::FromStr}; use cron_job::CronJobTarget; use mirrord_analytics::CollectAnalytics; +use replica_set::ReplicaSetTarget; use schemars::{gen::SchemaGenerator, schema::SchemaObject, JsonSchema}; use serde::{Deserialize, Serialize}; @@ -23,6 +23,7 @@ pub mod cron_job; pub mod deployment; pub mod job; pub mod pod; +pub mod replica_set; pub mod rollout; pub mod service; pub mod stateful_set; @@ -147,6 +148,7 @@ pub struct TargetConfig { /// - `statefulset/{statefulset-name}[/container/{container-name}]`; (requires mirrord /// Operator) /// - `service/{service-name}[/container/{container-name}]`; (requires mirrord Operator) + /// - `replicaset/{replicaset-name}[/container/{container-name}]`; (requires mirrord Operator) #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, @@ -221,6 +223,7 @@ mirrord-layer failed to parse the provided target! >> `cronjob/{cronjob-name}[/container/{container-name}]`; >> `statefulset/{statefulset-name}[/container/{container-name}]`; >> `service/{service-name}[/container/{container-name}]`; + >> `replicaset/{replicaset-name}[/container/{container-name}]`; - Note: >> specifying container name is optional, defaults to a container chosen by mirrord @@ -246,6 +249,7 @@ mirrord-layer failed to parse the provided target! /// - `cronjob/{cronjob-name}[/container/{container-name}]`; /// - `statefulset/{statefulset-name}[/container/{container-name}]`; /// - `service/{service-name}[/container/{container-name}]`; +/// - `replicaset/{replicaset-name}[/container/{container-name}]`; #[warn(clippy::wildcard_enum_match_arm)] #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug, JsonSchema)] #[serde(untagged, deny_unknown_fields)] @@ -282,6 +286,10 @@ pub enum Target { /// [Service](https://kubernetes.io/docs/concepts/services-networking/service/). Service(service::ServiceTarget), + /// + /// [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/). + ReplicaSet(replica_set::ReplicaSetTarget), + /// /// Spawn a new pod. Targetless, @@ -305,6 +313,7 @@ impl FromStr for Target { Some("cronjob") => cron_job::CronJobTarget::from_split(&mut split).map(Target::CronJob), Some("statefulset") => stateful_set::StatefulSetTarget::from_split(&mut split).map(Target::StatefulSet), Some("service") => service::ServiceTarget::from_split(&mut split).map(Target::Service), + Some("replicaset") => replica_set::ReplicaSetTarget::from_split(&mut split).map(Target::ReplicaSet), _ => Err(ConfigError::InvalidTarget(format!( "Provided target: {target} is unsupported. Did you remember to add a prefix, e.g. pod/{target}? \n{FAIL_PARSE_DEPLOYMENT_OR_POD}", ))), @@ -323,6 +332,7 @@ impl Target { Target::CronJob(target) => target.cron_job.clone(), Target::StatefulSet(target) => target.stateful_set.clone(), Target::Service(target) => target.service.clone(), + Target::ReplicaSet(target) => target.replica_set.clone(), Target::Targetless => { unreachable!("this shouldn't happen - called from operator on a flow where it's not targetless.") } @@ -338,7 +348,11 @@ impl Target { pub(super) fn requires_operator(&self) -> bool { matches!( self, - Target::Job(_) | Target::CronJob(_) | Target::StatefulSet(_) | Target::Service(_) + Target::Job(_) + | Target::CronJob(_) + | Target::StatefulSet(_) + | Target::Service(_) + | Target::ReplicaSet(_) ) } } @@ -399,6 +413,7 @@ impl_target_display!(JobTarget, job, "job"); impl_target_display!(CronJobTarget, cron_job, "cronjob"); impl_target_display!(StatefulSetTarget, stateful_set, "statefulset"); impl_target_display!(ServiceTarget, service, "service"); +impl_target_display!(ReplicaSetTarget, replica_set, "replicaset"); impl fmt::Display for Target { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -411,6 +426,7 @@ impl fmt::Display for Target { Target::CronJob(target) => target.fmt(f), Target::StatefulSet(target) => target.fmt(f), Target::Service(target) => target.fmt(f), + Target::ReplicaSet(target) => target.fmt(f), } } } @@ -426,6 +442,7 @@ impl TargetDisplay for Target { Target::CronJob(target) => target.type_(), Target::StatefulSet(target) => target.type_(), Target::Service(target) => target.type_(), + Target::ReplicaSet(target) => target.type_(), } } @@ -439,6 +456,7 @@ impl TargetDisplay for Target { Target::CronJob(target) => target.name(), Target::StatefulSet(target) => target.name(), Target::Service(target) => target.name(), + Target::ReplicaSet(target) => target.name(), } } @@ -452,6 +470,7 @@ impl TargetDisplay for Target { Target::CronJob(target) => target.container(), Target::StatefulSet(target) => target.container(), Target::Service(target) => target.container(), + Target::ReplicaSet(target) => target.container(), } } } @@ -469,6 +488,7 @@ bitflags::bitflags! { const CRON_JOB = 64; const STATEFUL_SET = 128; const SERVICE = 256; + const REPLICA_SET = 512; } } @@ -522,6 +542,12 @@ impl CollectAnalytics for &TargetConfig { flags |= TargetAnalyticFlags::CONTAINER; } } + Target::ReplicaSet(target) => { + flags |= TargetAnalyticFlags::REPLICA_SET; + if target.container.is_some() { + flags |= TargetAnalyticFlags::CONTAINER; + } + } Target::Targetless => { // Targetless is essentially 0, so no need to set any flags. } diff --git a/mirrord/config/src/target/replica_set.rs b/mirrord/config/src/target/replica_set.rs new file mode 100644 index 00000000000..08866b9f252 --- /dev/null +++ b/mirrord/config/src/target/replica_set.rs @@ -0,0 +1,35 @@ +use std::str::Split; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::{FromSplit, FAIL_PARSE_DEPLOYMENT_OR_POD}; +use crate::config::{ConfigError, Result}; + +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct ReplicaSetTarget { + pub replica_set: String, + pub container: Option, +} + +impl FromSplit for ReplicaSetTarget { + fn from_split(split: &mut Split) -> Result { + let replica_set = split + .next() + .ok_or_else(|| ConfigError::InvalidTarget(FAIL_PARSE_DEPLOYMENT_OR_POD.to_string()))?; + match (split.next(), split.next()) { + (Some("container"), Some(container)) => Ok(Self { + replica_set: replica_set.to_string(), + container: Some(container.to_string()), + }), + (None, None) => Ok(Self { + replica_set: replica_set.to_string(), + container: None, + }), + _ => Err(ConfigError::InvalidTarget( + FAIL_PARSE_DEPLOYMENT_OR_POD.to_string(), + )), + } + } +} diff --git a/mirrord/kube/src/api/container.rs b/mirrord/kube/src/api/container.rs index a651dc13458..49fba30e18b 100644 --- a/mirrord/kube/src/api/container.rs +++ b/mirrord/kube/src/api/container.rs @@ -53,8 +53,9 @@ impl ContainerParams { tls_cert: Option, pod_ips: Option, support_ipv6: bool, + port: Option, ) -> ContainerParams { - let port: u16 = rand::thread_rng().gen_range(30000..=65535); + let port = port.unwrap_or_else(|| rand::thread_rng().gen_range(30000..=65535)); let gid: u16 = rand::thread_rng().gen_range(3000..u16::MAX); let name = format!( diff --git a/mirrord/kube/src/api/kubernetes.rs b/mirrord/kube/src/api/kubernetes.rs index 44410f34b74..86a84f7e4ba 100644 --- a/mirrord/kube/src/api/kubernetes.rs +++ b/mirrord/kube/src/api/kubernetes.rs @@ -161,12 +161,15 @@ impl KubernetesAPI { /// * `tls_cert` - value for /// [`AGENT_OPERATOR_CERT_ENV`](mirrord_protocol::AGENT_OPERATOR_CERT_ENV), for creating an /// agent from the operator. In usage from this repo this is always `None`. + /// * `agent_port` - port number on which the agent will listen for client connections. If + /// [`None`] is given, a random high port will be user. #[tracing::instrument(level = "trace", skip(self), ret, err)] pub async fn create_agent_params( &self, target: &TargetConfig, tls_cert: Option, support_ipv6: bool, + agent_port: Option, ) -> Result<(ContainerParams, Option), KubeApiError> { let runtime_data = match target.path.as_ref().unwrap_or(&Target::Targetless) { Target::Targetless => None, @@ -188,7 +191,7 @@ impl KubernetesAPI { .join(",") }); - let params = ContainerParams::new(tls_cert, pod_ips, support_ipv6); + let params = ContainerParams::new(tls_cert, pod_ips, support_ipv6, agent_port); Ok((params, runtime_data)) } @@ -199,6 +202,8 @@ impl KubernetesAPI { /// * `tls_cert` - value for /// [`AGENT_OPERATOR_CERT_ENV`](mirrord_protocol::AGENT_OPERATOR_CERT_ENV), for creating an /// agent from the operator. In usage from this repo this is always `None`. + /// * `agent_port` - port number on which the agent will listen for client connections. If + /// [`None`] is given, a random high port will be used. #[tracing::instrument(level = "trace", skip(self, progress))] pub async fn create_agent

( &self, @@ -206,6 +211,7 @@ impl KubernetesAPI { target: &TargetConfig, config: Option<&LayerConfig>, tls_cert: Option, + agent_port: Option, ) -> Result where P: Progress + Send + Sync, @@ -214,7 +220,7 @@ impl KubernetesAPI { .map(|layer_conf| layer_conf.feature.network.ipv6) .unwrap_or_default(); let (params, runtime_data) = self - .create_agent_params(target, tls_cert, support_ipv6) + .create_agent_params(target, tls_cert, support_ipv6, agent_port) .await?; if let Some(RuntimeData { guessed_container: true, diff --git a/mirrord/kube/src/api/kubernetes/seeker.rs b/mirrord/kube/src/api/kubernetes/seeker.rs index e5208fcd8b5..5075bf61f4d 100644 --- a/mirrord/kube/src/api/kubernetes/seeker.rs +++ b/mirrord/kube/src/api/kubernetes/seeker.rs @@ -3,9 +3,9 @@ use std::fmt; use futures::{stream, Stream, StreamExt, TryStreamExt}; use k8s_openapi::{ api::{ - apps::v1::{Deployment, StatefulSet}, + apps::v1::{Deployment, ReplicaSet, StatefulSet}, batch::v1::{CronJob, Job}, - core::v1::Pod, + core::v1::{Pod, Service}, }, ClusterResourceScope, Metadata, NamespaceResourceScope, }; @@ -42,16 +42,25 @@ impl KubeResourceSeeker<'_> { .collect()) } - /// Returns all resource types ie. [`Pod`], [`Deployment`], [`Rollout`], [`Job`], [`CronJob`], - /// and [`StatefulSet`] + /// Returns all resource of all types: + /// 1. [`Deployment`]s, + /// 2. [`Rollout`]s, + /// 3. [`StatefulSet`]s + /// 4. [`CronJob`]s + /// 5. [`Job`]s + /// 6. [`Service`]s + /// 7. [`ReplicaSet`]s + /// 8. [`Pod`]s pub async fn all(&self) -> Result> { - let (pods, deployments, rollouts, jobs, cronjobs, statefulsets) = tokio::try_join!( + let (pods, deployments, rollouts, jobs, cronjobs, statefulsets, services, replicasets) = tokio::try_join!( self.pods(), self.simple_list_resource::("deployment"), self.simple_list_resource::("rollout"), self.simple_list_resource::("job"), self.simple_list_resource::("cronjob"), self.simple_list_resource::("statefulset"), + self.simple_list_resource::("service"), + self.simple_list_resource::("replicaset"), )?; Ok(deployments @@ -60,6 +69,8 @@ impl KubeResourceSeeker<'_> { .chain(statefulsets) .chain(cronjobs) .chain(jobs) + .chain(services) + .chain(replicasets) .chain(pods) .collect()) } diff --git a/mirrord/kube/src/api/runtime.rs b/mirrord/kube/src/api/runtime.rs index 19b36afecb5..01937a64d1b 100644 --- a/mirrord/kube/src/api/runtime.rs +++ b/mirrord/kube/src/api/runtime.rs @@ -33,6 +33,7 @@ pub mod cron_job; pub mod deployment; pub mod job; pub mod pod; +pub mod replica_set; pub mod rollout; pub mod service; pub mod stateful_set; @@ -374,6 +375,7 @@ impl RuntimeDataProvider for Target { Target::CronJob(target) => target.runtime_data(client, namespace).await, Target::StatefulSet(target) => target.runtime_data(client, namespace).await, Target::Service(target) => target.runtime_data(client, namespace).await, + Target::ReplicaSet(target) => target.runtime_data(client, namespace).await, Target::Targetless => Err(KubeApiError::MissingRuntimeData), } } @@ -389,6 +391,7 @@ impl RuntimeDataProvider for ResolvedTarget { Self::CronJob(target) => target.runtime_data(client, namespace).await, Self::StatefulSet(target) => target.runtime_data(client, namespace).await, Self::Service(target) => target.runtime_data(client, namespace).await, + Self::ReplicaSet(target) => target.runtime_data(client, namespace).await, Self::Targetless(_) => Err(KubeApiError::MissingRuntimeData), } } diff --git a/mirrord/kube/src/api/runtime/replica_set.rs b/mirrord/kube/src/api/runtime/replica_set.rs new file mode 100644 index 00000000000..a30f0764c5c --- /dev/null +++ b/mirrord/kube/src/api/runtime/replica_set.rs @@ -0,0 +1,27 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use k8s_openapi::api::apps::v1::StatefulSet; +use mirrord_config::target::replica_set::ReplicaSetTarget; + +use super::RuntimeDataFromLabels; +use crate::error::{KubeApiError, Result}; + +impl RuntimeDataFromLabels for ReplicaSetTarget { + type Resource = StatefulSet; + + fn name(&self) -> Cow { + Cow::from(&self.replica_set) + } + + fn container(&self) -> Option<&str> { + self.container.as_deref() + } + + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { + resource + .spec + .as_ref() + .and_then(|spec| spec.selector.match_labels.clone()) + .ok_or_else(|| KubeApiError::missing_field(resource, ".spec.selector.matchLabels")) + } +} diff --git a/mirrord/kube/src/resolved.rs b/mirrord/kube/src/resolved.rs index 70ba34d7535..c96b59e6962 100644 --- a/mirrord/kube/src/resolved.rs +++ b/mirrord/kube/src/resolved.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use k8s_openapi::api::{ - apps::v1::{Deployment, StatefulSet}, + apps::v1::{Deployment, ReplicaSet, StatefulSet}, batch::v1::{CronJob, Job}, core::v1::{Pod, Service}, }; @@ -19,6 +19,7 @@ pub mod cron_job; pub mod deployment; pub mod job; pub mod pod; +pub mod replica_set; pub mod rollout; pub mod service; pub mod stateful_set; @@ -41,6 +42,7 @@ pub enum ResolvedTarget { CronJob(ResolvedResource), StatefulSet(ResolvedResource), Service(ResolvedResource), + ReplicaSet(ResolvedResource), /// [`Pod`] is a special case, in that it does not implement [`RuntimeDataFromLabels`], /// and instead we implement a `runtime_data` method directly in its @@ -92,6 +94,9 @@ impl ResolvedTarget { ResolvedTarget::Service(ResolvedResource { resource, .. }) => { resource.metadata.name.as_deref() } + ResolvedTarget::ReplicaSet(ResolvedResource { resource, .. }) => { + resource.metadata.name.as_deref() + } ResolvedTarget::Targetless(_) => None, } } @@ -105,6 +110,7 @@ impl ResolvedTarget { ResolvedTarget::CronJob(ResolvedResource { resource, .. }) => resource.name_any(), ResolvedTarget::StatefulSet(ResolvedResource { resource, .. }) => resource.name_any(), ResolvedTarget::Service(ResolvedResource { resource, .. }) => resource.name_any(), + ResolvedTarget::ReplicaSet(ResolvedResource { resource, .. }) => resource.name_any(), ResolvedTarget::Targetless(..) => "targetless".to_string(), } } @@ -132,6 +138,9 @@ impl ResolvedTarget { ResolvedTarget::Service(ResolvedResource { resource, .. }) => { resource.metadata.namespace.as_deref() } + ResolvedTarget::ReplicaSet(ResolvedResource { resource, .. }) => { + resource.metadata.namespace.as_deref() + } ResolvedTarget::Targetless(namespace) => Some(namespace), } } @@ -150,6 +159,9 @@ impl ResolvedTarget { resource.metadata.labels } ResolvedTarget::Service(ResolvedResource { resource, .. }) => resource.metadata.labels, + ResolvedTarget::ReplicaSet(ResolvedResource { resource, .. }) => { + resource.metadata.labels + } ResolvedTarget::Targetless(_) => None, } } @@ -163,6 +175,7 @@ impl ResolvedTarget { ResolvedTarget::CronJob(_) => "cronjob", ResolvedTarget::StatefulSet(_) => "statefulset", ResolvedTarget::Service(_) => "service", + ResolvedTarget::ReplicaSet(_) => "replicaset", ResolvedTarget::Targetless(_) => "targetless", } } @@ -176,7 +189,10 @@ impl ResolvedTarget { | ResolvedTarget::CronJob(ResolvedResource { container, .. }) | ResolvedTarget::StatefulSet(ResolvedResource { container, .. }) | ResolvedTarget::Service(ResolvedResource { container, .. }) - | ResolvedTarget::Pod(ResolvedResource { container, .. }) => container.as_deref(), + | ResolvedTarget::Pod(ResolvedResource { container, .. }) + | ResolvedTarget::ReplicaSet(ResolvedResource { container, .. }) => { + container.as_deref() + } ResolvedTarget::Targetless(..) => None, } } @@ -268,6 +284,15 @@ impl ResolvedTarget { container: target.container.clone(), }) }), + Target::ReplicaSet(target) => get_k8s_resource_api::(client, namespace) + .get(&target.replica_set) + .await + .map(|resource| { + ResolvedTarget::ReplicaSet(ResolvedResource { + resource, + container: target.container.clone(), + }) + }), Target::Targetless => Ok(ResolvedTarget::Targetless( namespace.unwrap_or("default").to_string(), )), @@ -463,6 +488,43 @@ impl ResolvedTarget { })) } + ResolvedTarget::ReplicaSet(ResolvedResource { + resource, + container, + }) => { + let available = resource + .status + .as_ref() + .ok_or_else(|| KubeApiError::missing_field(&resource, ".status"))? + .available_replicas + .unwrap_or_default(); // Field can be missing when there are no replicas + + if available <= 0 { + return Err(KubeApiError::invalid_state( + &resource, + "no available replicas", + )); + } + + if let Some(container) = &container { + // verify that the container exists + resource + .spec + .as_ref() + .and_then(|spec| spec.template.as_ref()?.spec.as_ref()) + .ok_or_else(|| KubeApiError::missing_field(&resource, ".spec.template.spec"))? + .containers + .iter() + .find(|c| c.name == *container) + .ok_or_else(|| KubeApiError::invalid_state(&resource, format_args!("specified pod template does not contain target container `{container}`")))?; + } + + Ok(ResolvedTarget::ReplicaSet(ResolvedResource { + resource, + container, + })) + } + ResolvedTarget::Targetless(namespace) => { // no check needed here Ok(ResolvedTarget::Targetless(namespace)) diff --git a/mirrord/kube/src/resolved/replica_set.rs b/mirrord/kube/src/resolved/replica_set.rs new file mode 100644 index 00000000000..9a200779520 --- /dev/null +++ b/mirrord/kube/src/resolved/replica_set.rs @@ -0,0 +1,31 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use k8s_openapi::api::apps::v1::ReplicaSet; + +use super::{ResolvedResource, RuntimeDataFromLabels}; +use crate::error::{KubeApiError, Result}; + +impl RuntimeDataFromLabels for ResolvedResource { + type Resource = ReplicaSet; + + fn name(&self) -> Cow { + self.resource + .metadata + .name + .as_ref() + .map(Cow::from) + .unwrap_or_default() + } + + fn container(&self) -> Option<&str> { + self.container.as_deref() + } + + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { + resource + .spec + .as_ref() + .and_then(|spec| spec.selector.match_labels.clone()) + .ok_or_else(|| KubeApiError::missing_field(resource, ".spec.selector.matchLabels")) + } +} diff --git a/mirrord/layer/src/file/hooks.rs b/mirrord/layer/src/file/hooks.rs index 3fcfc3c1280..289b10a34d6 100644 --- a/mirrord/layer/src/file/hooks.rs +++ b/mirrord/layer/src/file/hooks.rs @@ -906,7 +906,7 @@ unsafe extern "C" fn fstatat_detour( /// Hook for `libc::fstatfs`. #[hook_guard_fn] -pub(crate) unsafe extern "C" fn fstatfs_detour(fd: c_int, out_stat: *mut statfs) -> c_int { +unsafe extern "C" fn fstatfs_detour(fd: c_int, out_stat: *mut statfs) -> c_int { if out_stat.is_null() { return HookError::BadPointer.into(); } @@ -922,10 +922,7 @@ pub(crate) unsafe extern "C" fn fstatfs_detour(fd: c_int, out_stat: *mut statfs) /// Hook for `libc::statfs`. #[hook_guard_fn] -pub(crate) unsafe extern "C" fn statfs_detour( - raw_path: *const c_char, - out_stat: *mut statfs, -) -> c_int { +unsafe extern "C" fn statfs_detour(raw_path: *const c_char, out_stat: *mut statfs) -> c_int { if out_stat.is_null() { return HookError::BadPointer.into(); } diff --git a/mirrord/layer/src/go/linux_x64.rs b/mirrord/layer/src/go/linux_x64.rs index 622a24383d7..2b2cf5870ee 100644 --- a/mirrord/layer/src/go/linux_x64.rs +++ b/mirrord/layer/src/go/linux_x64.rs @@ -340,8 +340,10 @@ unsafe extern "C" fn c_abi_syscall_handler( faccessat_detour(param1 as _, param2 as _, param3 as _, 0) as i64 } libc::SYS_fstat => fstat_detour(param1 as _, param2 as _) as i64, - libc::SYS_statfs => statfs_detour(param1 as _, param2 as _) as i64, - libc::SYS_fstatfs => fstatfs_detour(param1 as _, param2 as _) as i64, + // Currently disabled due to a [bug](https://github.com/metalbear-co/mirrord/issues/3044). + // libc::SYS_statfs => statfs_detour(param1 as _, param2 as _) as i64, + // Currently disabled due to a [bug](https://github.com/metalbear-co/mirrord/issues/3044). + // libc::SYS_fstatfs => fstatfs_detour(param1 as _, param2 as _) as i64, libc::SYS_getdents64 => getdents64_detour(param1 as _, param2 as _, param3 as _) as i64, #[cfg(all(target_os = "linux", not(target_arch = "aarch64")))] libc::SYS_mkdir => mkdir_detour(param1 as _, param2 as _) as i64, diff --git a/mirrord/layer/src/go/mod.rs b/mirrord/layer/src/go/mod.rs index 6a28c3ebfd9..d5a596f2259 100644 --- a/mirrord/layer/src/go/mod.rs +++ b/mirrord/layer/src/go/mod.rs @@ -101,8 +101,10 @@ unsafe extern "C" fn c_abi_syscall6_handler( .into() } libc::SYS_fstat => fstat_detour(param1 as _, param2 as _) as i64, - libc::SYS_statfs => statfs_detour(param1 as _, param2 as _) as i64, - libc::SYS_fstatfs => fstatfs_detour(param1 as _, param2 as _) as i64, + // Currently disabled due to a [bug](https://github.com/metalbear-co/mirrord/issues/3044). + // libc::SYS_statfs => statfs_detour(param1 as _, param2 as _) as i64, + // Currently disabled due to a [bug](https://github.com/metalbear-co/mirrord/issues/3044). + // libc::SYS_fstatfs => fstatfs_detour(param1 as _, param2 as _) as i64, libc::SYS_fsync => fsync_detour(param1 as _) as i64, libc::SYS_fdatasync => fsync_detour(param1 as _) as i64, libc::SYS_openat => { diff --git a/mirrord/layer/tests/apps/fileops/go/main.go b/mirrord/layer/tests/apps/fileops/go/main.go index 69db336fb3e..16ef4b33009 100644 --- a/mirrord/layer/tests/apps/fileops/go/main.go +++ b/mirrord/layer/tests/apps/fileops/go/main.go @@ -7,21 +7,23 @@ import ( func main() { tempFile := "/tmp/test_file.txt" - fd, _ := syscall.Open(tempFile, syscall.O_CREAT|syscall.O_WRONLY, 0644) + syscall.Open(tempFile, syscall.O_CREAT|syscall.O_WRONLY, 0644) var stat syscall.Stat_t err := syscall.Stat(tempFile, &stat) if err != nil { panic(err) } - var statfs syscall.Statfs_t - err = syscall.Statfs(tempFile, &statfs) - if err != nil { - panic(err) - } + // statfs/fstatfs hooks are currently disabled in Go apps due to a [bug](https://github.com/metalbear-co/mirrord/issues/3044). - err = syscall.Fstatfs(fd, &statfs) - if err != nil { - panic(err) - } + // var statfs syscall.Statfs_t + // err = syscall.Statfs(tempFile, &statfs) + // if err != nil { + // panic(err) + // } + + // err = syscall.Fstatfs(fd, &statfs) + // if err != nil { + // panic(err) + // } } diff --git a/mirrord/layer/tests/fileops.rs b/mirrord/layer/tests/fileops.rs index de26b318f40..315542717c0 100644 --- a/mirrord/layer/tests/fileops.rs +++ b/mirrord/layer/tests/fileops.rs @@ -345,8 +345,9 @@ async fn go_stat( )))) .await; - intproxy.expect_statfs("/tmp/test_file.txt").await; - intproxy.expect_fstatfs(fd).await; + // statfs/fstatfs hooks are currently disabled in Go apps due to a [bug](https://github.com/metalbear-co/mirrord/issues/3044). + // intproxy.expect_statfs("/tmp/test_file.txt").await; + // intproxy.expect_fstatfs(fd).await; test_process.wait_assert_success().await; test_process.assert_no_error_in_stderr().await; diff --git a/mirrord/operator/src/crd.rs b/mirrord/operator/src/crd.rs index efae6aa6751..a8c3c13676a 100644 --- a/mirrord/operator/src/crd.rs +++ b/mirrord/operator/src/crd.rs @@ -64,6 +64,7 @@ impl TargetCrd { Target::CronJob(target) => ("cronjob", &target.cron_job, &target.container), Target::StatefulSet(target) => ("statefulset", &target.stateful_set, &target.container), Target::Service(target) => ("service", &target.service, &target.container), + Target::ReplicaSet(target) => ("replicaset", &target.replica_set, &target.container), Target::Targetless => return TARGETLESS_TARGET_NAME.to_string(), }; diff --git a/mirrord/operator/src/setup.rs b/mirrord/operator/src/setup.rs index 80c09c0b36c..c345a43a02b 100644 --- a/mirrord/operator/src/setup.rs +++ b/mirrord/operator/src/setup.rs @@ -545,6 +545,7 @@ impl OperatorClusterRole { "statefulsets".to_owned(), "statefulsets/scale".to_owned(), "services".to_owned(), + "replicasets".to_owned(), ]), verbs: vec!["get".to_owned(), "list".to_owned(), "watch".to_owned()], ..Default::default() @@ -591,7 +592,11 @@ impl OperatorClusterRole { // Allow for patching replicas and environment variables. PolicyRule { api_groups: Some(vec!["apps".to_owned()]), - resources: Some(vec!["deployments".to_owned(), "statefulsets".to_owned()]), + resources: Some(vec![ + "deployments".to_owned(), + "statefulsets".to_owned(), + "replicasets".to_owned(), + ]), verbs: vec!["patch".to_owned()], ..Default::default() }, diff --git a/tests/src/operator/sanity.rs b/tests/src/operator/sanity.rs index e9264a3681a..b219539f4f5 100644 --- a/tests/src/operator/sanity.rs +++ b/tests/src/operator/sanity.rs @@ -16,7 +16,22 @@ pub async fn mirrord_ls(#[future] service_for_mirrord_ls: KubeService) { assert!(res.success()); let stdout = process.get_stdout().await; let targets: Vec = serde_json::from_str(&stdout).unwrap(); - let re = Regex::new(r"^(pod|deployment|statefulset|cronjob|job)/.+(/container/.+)?$").unwrap(); + + let expected_target_types = [ + "pod", + "deployment", + "statefulset", + "cronjob", + "job", + "replicaset", + "service", + ]; + + let re = Regex::new(&format!( + r"^({})/.+(/container/.+)?$", + expected_target_types.join("|") + )) + .unwrap(); targets.iter().for_each(|output| { assert!( re.is_match(output), @@ -24,7 +39,7 @@ pub async fn mirrord_ls(#[future] service_for_mirrord_ls: KubeService) { ); }); - for target_type in ["pod", "deployment", "statefulset", "cronjob", "job"] { + for target_type in expected_target_types { assert!( targets .iter()