diff --git a/changelog.d/+service-target.added.md b/changelog.d/+service-target.added.md new file mode 100644 index 00000000000..5d1171eae3e --- /dev/null +++ b/changelog.d/+service-target.added.md @@ -0,0 +1 @@ +Added Kubernetes Service as a new type of mirrord target (requires mirrord operator). diff --git a/changelog.d/3009.fixed.md b/changelog.d/3009.fixed.md new file mode 100644 index 00000000000..db44c56fc79 --- /dev/null +++ b/changelog.d/3009.fixed.md @@ -0,0 +1 @@ +Fixed misleading doc for `.target.namespace` config. diff --git a/mirrord-schema.json b/mirrord-schema.json index 19577b952af..dee629fe493 100644 --- a/mirrord-schema.json +++ b/mirrord-schema.json @@ -1689,6 +1689,24 @@ }, "additionalProperties": false }, + "ServiceTarget": { + "type": "object", + "required": [ + "service" + ], + "properties": { + "container": { + "type": [ + "string", + "null" + ] + }, + "service": { + "type": "string" + } + }, + "additionalProperties": false + }, "SplitQueuesConfig": { "description": "```json { \"feature\": { \"split_queues\": { \"first-queue\": { \"queue_type\": \"SQS\", \"message_filter\": { \"wows\": \"so wows\", \"coolz\": \"^very\" } }, \"second-queue\": { \"queue_type\": \"SQS\", \"message_filter\": { \"who\": \"you$\" } }, \"third-queue\": { \"queue_type\": \"Kafka\", \"message_filter\": { \"who\": \"you$\" } }, \"fourth-queue\": { \"queue_type\": \"Kafka\", \"message_filter\": { \"wows\": \"so wows\", \"coolz\": \"^very\" } }, } } } ```", "type": "object", @@ -1715,10 +1733,10 @@ "additionalProperties": false }, "Target": { - "description": " ## path\n\nSpecifies the running pod (or deployment) to mirror.\n\nSupports: - `pod/{sample-pod}`; - `deployment/{sample-deployment}`; - `container/{sample-container}`; - `containername/{sample-container}`. - `job/{sample-job}`; - `cronjob/{sample-cronjob}`; - `statefulset/{sample-statefulset}`;", + "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}]`;", "anyOf": [ { - "description": " Mirror a deployment.", + "description": " [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/).", "allOf": [ { "$ref": "#/definitions/DeploymentTarget" @@ -1726,7 +1744,7 @@ ] }, { - "description": " Mirror a pod.", + "description": " [Pod](https://kubernetes.io/docs/concepts/workloads/pods/).", "allOf": [ { "$ref": "#/definitions/PodTarget" @@ -1734,7 +1752,7 @@ ] }, { - "description": " Mirror a rollout.", + "description": " [Argo Rollout](https://argoproj.github.io/argo-rollouts/#how-does-it-work).", "allOf": [ { "$ref": "#/definitions/RolloutTarget" @@ -1742,7 +1760,7 @@ ] }, { - "description": " Mirror a Job.\n\nOnly supported when `copy_target` is enabled.", + "description": " [Job](https://kubernetes.io/docs/concepts/workloads/controllers/job/).\n\nOnly supported when `copy_target` is enabled.", "allOf": [ { "$ref": "#/definitions/JobTarget" @@ -1750,7 +1768,7 @@ ] }, { - "description": " Targets a [CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/).\n\nOnly supported when `copy_target` is enabled.", + "description": " [CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/).\n\nOnly supported when `copy_target` is enabled.", "allOf": [ { "$ref": "#/definitions/CronJobTarget" @@ -1758,13 +1776,21 @@ ] }, { - "description": " Targets a [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/).\n\nOnly supported when `copy_target` is enabled.", + "description": " [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/).", "allOf": [ { "$ref": "#/definitions/StatefulSetTarget" } ] }, + { + "description": " [Service](https://kubernetes.io/docs/concepts/services-networking/service/).", + "allOf": [ + { + "$ref": "#/definitions/ServiceTarget" + } + ] + }, { "description": " Spawn a new pod.", "type": "null" diff --git a/mirrord/cli/src/verify_config.rs b/mirrord/cli/src/verify_config.rs index 56d719dacbc..4628118360b 100644 --- a/mirrord/cli/src/verify_config.rs +++ b/mirrord/cli/src/verify_config.rs @@ -8,7 +8,8 @@ use mirrord_config::{ feature::FeatureConfig, target::{ cron_job::CronJobTarget, deployment::DeploymentTarget, job::JobTarget, pod::PodTarget, - rollout::RolloutTarget, stateful_set::StatefulSetTarget, Target, TargetConfig, + rollout::RolloutTarget, service::ServiceTarget, stateful_set::StatefulSetTarget, Target, + TargetConfig, }, }; use serde::Serialize; @@ -43,6 +44,9 @@ enum VerifiedTarget { #[serde(untagged)] StatefulSet(StatefulSetTarget), + + #[serde(untagged)] + Service(ServiceTarget), } impl From for VerifiedTarget { @@ -54,6 +58,7 @@ impl From for VerifiedTarget { Target::Job(target) => Self::Job(target), Target::CronJob(target) => Self::CronJob(target), Target::StatefulSet(target) => Self::StatefulSet(target), + Target::Service(target) => Self::Service(target), Target::Targetless => Self::Targetless, } } @@ -69,6 +74,7 @@ impl From for TargetType { VerifiedTarget::Job(_) => TargetType::Job, VerifiedTarget::CronJob(_) => TargetType::CronJob, VerifiedTarget::StatefulSet(_) => TargetType::StatefulSet, + VerifiedTarget::Service(_) => TargetType::Service, } } } @@ -99,6 +105,7 @@ enum TargetType { Job, CronJob, StatefulSet, + Service, } impl core::fmt::Display for TargetType { @@ -111,6 +118,7 @@ impl core::fmt::Display for TargetType { TargetType::Job => "job", TargetType::CronJob => "cronjob", TargetType::StatefulSet => "statefulset", + TargetType::Service => "service", }; f.write_str(stringifed) @@ -127,6 +135,7 @@ impl TargetType { Self::Job, Self::CronJob, Self::StatefulSet, + Self::Service, ] .into_iter() } @@ -136,6 +145,7 @@ impl TargetType { Self::Targetless | Self::Rollout => !config.copy_target.enabled, 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, } } diff --git a/mirrord/config/configuration.md b/mirrord/config/configuration.md index 0d1af19401d..e8f3f43c437 100644 --- a/mirrord/config/configuration.md +++ b/mirrord/config/configuration.md @@ -1561,13 +1561,23 @@ Accepts a single value, or multiple values separated by `;`. ## target {#root-target} -Specifies the target and namespace to mirror, see [`path`](#target-path) for a list of -accepted values for the `target` option. +Specifies the target and namespace to target. The simplified configuration supports: -- `pod/{sample-pod}/[container]/{sample-container}`; -- `deployment/{sample-deployment}/[container]/{sample-container}`; +- `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}]`; + +Please note that: + +- `job`, `cronjob`, `statefulset` and `service` targets require the mirrord Operator +- `job` and `cronjob` targets require the [`copy_target`](#feature-copy_target) feature Shortened setup: @@ -1577,38 +1587,63 @@ Shortened setup: } ``` +The setup above will result in a session targeting the `bear-pod` Kubernetes pod +in the user's default namespace. A target container will be chosen by mirrord. + +Shortened setup with target container: + +```json +{ + "target": "pod/bear-pod/container/bear-pod-container" +} +``` + +The setup above will result in a session targeting the `bear-pod-container` container +in the `bear-pod` Kubernetes pod in the user's default namespace. + Complete setup: ```json { "target": { "path": { - "pod": "bear-pod" + "pod": "bear-pod", + "container": "bear-pod-container" }, - "namespace": "default" + "namespace": "bear-pod-namespace" } } ``` +The setup above will result in a session targeting the `bear-pod-container` container +in the `bear-pod` Kubernetes pod in the `bear-pod-namespace` namespace. + ### target.namespace {#target-namespace} Namespace where the target lives. -Defaults to `"default"`. +Defaults to the Kubernetes user's default namespace (defined in Kubernetes context). ### target.path {#target-path} -Specifies the running pod (or deployment) to mirror. +Specifies the Kubernetes resource to target. -Note: Deployment level steal/mirroring is available only in mirrord for Teams -If you use it without it, it will choose a random pod replica to work with. +Note: targeting services and whole workloads is available only in mirrord for Teams. +If you target a workload without the mirrord Operator, it will choose a random pod replica +to work with. Supports: -- `pod/{sample-pod}`; -- `deployment/{sample-deployment}`; -- `container/{sample-container}`; -- `containername/{sample-container}`. -- `job/{sample-job}` (only when [`copy_target`](#feature-copy_target) is enabled). +- `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}]`; (requires mirrord Operator and the + [`copy_target`](#feature-copy_target) feature) +- `cronjob/{cronjob-name}[/container/{container-name}]`; (requires mirrord Operator and the + [`copy_target`](#feature-copy_target) feature) +- `statefulset/{statefulset-name}[/container/{container-name}]`; (requires mirrord + Operator) +- `service/{service-name}[/container/{container-name}]`; (requires mirrord Operator) ## telemetry {#root-telemetry} Controls whether or not mirrord sends telemetry data to MetalBear cloud. diff --git a/mirrord/config/src/lib.rs b/mirrord/config/src/lib.rs index 7ca3e00dc39..d0384d834b5 100644 --- a/mirrord/config/src/lib.rs +++ b/mirrord/config/src/lib.rs @@ -540,6 +540,14 @@ impl LayerConfig { )); } + if matches!(self.target.path, Some(Target::Service(..))) { + return Err(ConfigError::Conflict( + "The copy target feature is not yet supported with service targets, \ + please either disable this option or specify an exact workload covered by this service." + .into() + )); + } + if !self.feature.network.incoming.is_steal() { context.add_warning( "Using copy target feature without steal mode \ diff --git a/mirrord/config/src/target.rs b/mirrord/config/src/target.rs index e262ee4cafc..d62e0ae1053 100644 --- a/mirrord/config/src/target.rs +++ b/mirrord/config/src/target.rs @@ -5,9 +5,11 @@ use cron_job::CronJobTarget; use mirrord_analytics::CollectAnalytics; use schemars::{gen::SchemaGenerator, schema::SchemaObject, JsonSchema}; use serde::{Deserialize, Serialize}; -use stateful_set::StatefulSetTarget; -use self::{deployment::DeploymentTarget, job::JobTarget, pod::PodTarget, rollout::RolloutTarget}; +use self::{ + deployment::DeploymentTarget, job::JobTarget, pod::PodTarget, rollout::RolloutTarget, + service::ServiceTarget, stateful_set::StatefulSetTarget, +}; use crate::{ config::{ from_env::{FromEnv, FromEnvWithError}, @@ -22,6 +24,7 @@ pub mod deployment; pub mod job; pub mod pod; pub mod rollout; +pub mod service; pub mod stateful_set; #[derive(Deserialize, PartialEq, Eq, Clone, Debug, JsonSchema)] @@ -65,19 +68,23 @@ fn make_simple_target_custom_schema(gen: &mut SchemaGenerator) -> schemars::sche schema.into() } -// - Only path is `Some` -> use current namespace. -// - Only namespace is `Some` -> this should only happen in `mirrord ls`. In `mirrord exec` -// namespace without a path does not mean anything and therefore should be prevented by returning -// an error. The error is not returned when parsing the configuration because it's not an error -// for `mirrord ls`. -// - Both are `None` -> targetless. -/// Specifies the target and namespace to mirror, see [`path`](#target-path) for a list of -/// accepted values for the `target` option. +/// Specifies the target and namespace to target. /// /// The simplified configuration supports: /// -/// - `pod/{sample-pod}/[container]/{sample-container}`; -/// - `deployment/{sample-deployment}/[container]/{sample-container}`; +/// - `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}]`; +/// +/// Please note that: +/// +/// - `job`, `cronjob`, `statefulset` and `service` targets require the mirrord Operator +/// - `job` and `cronjob` targets require the [`copy_target`](#feature-copy_target) feature /// /// Shortened setup: /// @@ -87,34 +94,59 @@ fn make_simple_target_custom_schema(gen: &mut SchemaGenerator) -> schemars::sche /// } /// ``` /// +/// The setup above will result in a session targeting the `bear-pod` Kubernetes pod +/// in the user's default namespace. A target container will be chosen by mirrord. +/// +/// Shortened setup with target container: +/// +/// ```json +/// { +/// "target": "pod/bear-pod/container/bear-pod-container" +/// } +/// ``` +/// +/// The setup above will result in a session targeting the `bear-pod-container` container +/// in the `bear-pod` Kubernetes pod in the user's default namespace. +/// /// Complete setup: /// /// ```json /// { /// "target": { /// "path": { -/// "pod": "bear-pod" +/// "pod": "bear-pod", +/// "container": "bear-pod-container" /// }, -/// "namespace": "default" +/// "namespace": "bear-pod-namespace" /// } /// } /// ``` +/// +/// The setup above will result in a session targeting the `bear-pod-container` container +/// in the `bear-pod` Kubernetes pod in the `bear-pod-namespace` namespace. #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug)] #[serde(deny_unknown_fields)] pub struct TargetConfig { /// ### target.path {#target-path} /// - /// Specifies the running pod (or deployment) to mirror. + /// Specifies the Kubernetes resource to target. /// - /// Note: Deployment level steal/mirroring is available only in mirrord for Teams - /// If you use it without it, it will choose a random pod replica to work with. + /// Note: targeting services and whole workloads is available only in mirrord for Teams. + /// If you target a workload without the mirrord Operator, it will choose a random pod replica + /// to work with. /// /// Supports: - /// - `pod/{sample-pod}`; - /// - `deployment/{sample-deployment}`; - /// - `container/{sample-container}`; - /// - `containername/{sample-container}`. - /// - `job/{sample-job}` (only when [`copy_target`](#feature-copy_target) is enabled). + /// - `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}]`; (requires mirrord Operator and the + /// [`copy_target`](#feature-copy_target) feature) + /// - `cronjob/{cronjob-name}[/container/{container-name}]`; (requires mirrord Operator and the + /// [`copy_target`](#feature-copy_target) feature) + /// - `statefulset/{statefulset-name}[/container/{container-name}]`; (requires mirrord + /// Operator) + /// - `service/{service-name}[/container/{container-name}]`; (requires mirrord Operator) #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, @@ -122,7 +154,7 @@ pub struct TargetConfig { /// /// Namespace where the target lives. /// - /// Defaults to `"default"`. + /// Defaults to the Kubernetes user's default namespace (defined in Kubernetes context). #[serde(skip_serializing_if = "Option::is_none")] pub namespace: Option, } @@ -181,16 +213,18 @@ const FAIL_PARSE_DEPLOYMENT_OR_POD: &str = r#" mirrord-layer failed to parse the provided target! - Valid format: - >> deployment/[/container/container-name] - >> deploy/[/container/container-name] - >> pod/[/container/container-name] - >> job/[/container/container-name] - >> cronjob/[/container/container-name] - >> statefulset/[/container/container-name] + >> `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}]`; - Note: - >> specifying container name is optional, defaults to the first container in the provided pod/deployment target. - >> specifying the pod name is optional, defaults to the first pod in case the target is a deployment. + >> specifying container name is optional, defaults to a container chosen by mirrord + >> targeting a workload without the mirrord Operator results in a session targeting a random pod replica - Suggestions: >> check for typos in the provided target. @@ -204,49 +238,50 @@ mirrord-layer failed to parse the provided target! /// Specifies the running pod (or deployment) to mirror. /// /// Supports: -/// - `pod/{sample-pod}`; -/// - `deployment/{sample-deployment}`; -/// - `container/{sample-container}`; -/// - `containername/{sample-container}`. -/// - `job/{sample-job}`; -/// - `cronjob/{sample-cronjob}`; -/// - `statefulset/{sample-statefulset}`; +/// - `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}]`; #[warn(clippy::wildcard_enum_match_arm)] #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Hash, Debug, JsonSchema)] #[serde(untagged, deny_unknown_fields)] pub enum Target { /// - /// Mirror a deployment. + /// [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/). Deployment(deployment::DeploymentTarget), /// - /// Mirror a pod. + /// [Pod](https://kubernetes.io/docs/concepts/workloads/pods/). Pod(pod::PodTarget), /// - /// Mirror a rollout. + /// [Argo Rollout](https://argoproj.github.io/argo-rollouts/#how-does-it-work). Rollout(rollout::RolloutTarget), /// - /// Mirror a Job. + /// [Job](https://kubernetes.io/docs/concepts/workloads/controllers/job/). /// /// Only supported when `copy_target` is enabled. Job(job::JobTarget), /// - /// Targets a /// [CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/). /// /// Only supported when `copy_target` is enabled. CronJob(cron_job::CronJobTarget), /// - /// Targets a /// [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/). - /// - /// Only supported when `copy_target` is enabled. StatefulSet(stateful_set::StatefulSetTarget), + /// + /// [Service](https://kubernetes.io/docs/concepts/services-networking/service/). + Service(service::ServiceTarget), + /// /// Spawn a new pod. Targetless, @@ -269,6 +304,7 @@ impl FromStr for Target { Some("job") => job::JobTarget::from_split(&mut split).map(Target::Job), 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), _ => 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}", ))), @@ -286,6 +322,7 @@ impl Target { Target::Job(target) => target.job.clone(), Target::CronJob(target) => target.cron_job.clone(), Target::StatefulSet(target) => target.stateful_set.clone(), + Target::Service(target) => target.service.clone(), Target::Targetless => { unreachable!("this shouldn't happen - called from operator on a flow where it's not targetless.") } @@ -301,7 +338,7 @@ impl Target { pub(super) fn requires_operator(&self) -> bool { matches!( self, - Target::Job(_) | Target::CronJob(_) | Target::StatefulSet(_) + Target::Job(_) | Target::CronJob(_) | Target::StatefulSet(_) | Target::Service(_) ) } } @@ -361,6 +398,7 @@ impl_target_display!(RolloutTarget, rollout, "rollout"); 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 fmt::Display for Target { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -372,6 +410,7 @@ impl fmt::Display for Target { Target::Job(target) => target.fmt(f), Target::CronJob(target) => target.fmt(f), Target::StatefulSet(target) => target.fmt(f), + Target::Service(target) => target.fmt(f), } } } @@ -386,6 +425,7 @@ impl TargetDisplay for Target { Target::Job(target) => target.type_(), Target::CronJob(target) => target.type_(), Target::StatefulSet(target) => target.type_(), + Target::Service(target) => target.type_(), } } @@ -398,6 +438,7 @@ impl TargetDisplay for Target { Target::Job(target) => target.name(), Target::CronJob(target) => target.name(), Target::StatefulSet(target) => target.name(), + Target::Service(target) => target.name(), } } @@ -410,6 +451,7 @@ impl TargetDisplay for Target { Target::Job(target) => target.container(), Target::CronJob(target) => target.container(), Target::StatefulSet(target) => target.container(), + Target::Service(target) => target.container(), } } } @@ -426,6 +468,7 @@ bitflags::bitflags! { const JOB = 32; const CRON_JOB = 64; const STATEFUL_SET = 128; + const SERVICE = 256; } } @@ -473,6 +516,12 @@ impl CollectAnalytics for &TargetConfig { flags |= TargetAnalyticFlags::CONTAINER; } } + Target::Service(target) => { + flags |= TargetAnalyticFlags::SERVICE; + 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/service.rs b/mirrord/config/src/target/service.rs new file mode 100644 index 00000000000..8d41e0053fe --- /dev/null +++ b/mirrord/config/src/target/service.rs @@ -0,0 +1,36 @@ +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 ServiceTarget { + pub service: String, + pub container: Option, +} + +impl FromSplit for ServiceTarget { + fn from_split(split: &mut Split) -> Result { + let service = 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 { + service: service.to_string(), + container: Some(container.to_string()), + }), + (None, None) => Ok(Self { + service: service.to_string(), + container: None, + }), + _ => Err(ConfigError::InvalidTarget( + FAIL_PARSE_DEPLOYMENT_OR_POD.to_string(), + )), + } + } +} diff --git a/mirrord/kube/src/api/runtime.rs b/mirrord/kube/src/api/runtime.rs index a431a3b4c03..19b36afecb5 100644 --- a/mirrord/kube/src/api/runtime.rs +++ b/mirrord/kube/src/api/runtime.rs @@ -3,6 +3,7 @@ use std::{ collections::BTreeMap, convert::Infallible, fmt::{self, Display, Formatter}, + future::Future, net::IpAddr, ops::FromResidual, str::FromStr, @@ -17,6 +18,7 @@ use mirrord_config::target::Target; use mirrord_protocol::MeshVendor; use serde::de::DeserializeOwned; use thiserror::Error; +use tracing::Level; use crate::{ api::{ @@ -32,6 +34,7 @@ pub mod deployment; pub mod job; pub mod pod; pub mod rollout; +pub mod service; pub mod stateful_set; #[derive(Debug)] @@ -207,7 +210,7 @@ impl RuntimeData { }) } - #[tracing::instrument(level = "trace", skip(client), ret)] + #[tracing::instrument(level = Level::TRACE, skip(client), ret)] pub async fn check_node(&self, client: &kube::Client) -> NodeCheck { let node_api: Api = Api::all(client.clone()); let pod_api: Api = Api::all(client.clone()); @@ -271,20 +274,61 @@ where } pub trait RuntimeDataProvider { - #[allow(async_fn_in_trait)] - async fn runtime_data(&self, client: &Client, namespace: Option<&str>) -> Result; + fn runtime_data( + &self, + client: &Client, + namespace: Option<&str>, + ) -> impl Future>; } +/// Trait for resources that abstract a set of pods +/// defined by a label selector. +/// +/// Implementors are provided with an implementation of [`RuntimeDataProvider`]. +/// When resolving [`RuntimeData`], the set of pods is fetched and [`RuntimeData`] is extracted from +/// the first pod on the list. If the set is empty, resolution fails. pub trait RuntimeDataFromLabels { type Resource: Resource + Clone + DeserializeOwned + fmt::Debug; - #[allow(async_fn_in_trait)] - async fn get_selector_match_labels( + fn get_selector_match_labels(resource: &Self::Resource) -> Result>; + + /// Returns a list of pods matching the selector of the given `resource`. + fn get_pods( resource: &Self::Resource, - ) -> Result>; + client: &Client, + ) -> impl Future>> { + async { + let api: Api<::Resource> = + get_k8s_resource_api(client, resource.meta().namespace.as_deref()); + let name = resource + .meta() + .name + .as_deref() + .ok_or_else(|| KubeApiError::missing_field(resource, ".metadata.name"))?; + let resource = api.get(name).await?; + + let labels = Self::get_selector_match_labels(&resource)?; + + let formatted_labels = labels + .iter() + .map(|(key, value)| format!("{key}={value}")) + .collect::>() + .join(","); + let list_params = ListParams { + label_selector: Some(formatted_labels), + ..Default::default() + }; + + let pod_api: Api = + get_k8s_resource_api(client, resource.meta().namespace.as_deref()); + let pods = pod_api.list(&list_params).await?; + + Ok(pods.items) + } + } fn name(&self) -> Cow; @@ -299,37 +343,22 @@ where let api: Api<::Resource> = get_k8s_resource_api(client, namespace); let resource = api.get(&self.name()).await?; + let pods = Self::get_pods(&resource, client).await?; - let labels = Self::get_selector_match_labels(&resource).await?; - - let formatted_labels = labels - .iter() - .map(|(key, value)| format!("{key}={value}")) - .collect::>() - .join(","); - let list_params = ListParams { - label_selector: Some(formatted_labels), - ..Default::default() - }; - - let pod_api: Api = get_k8s_resource_api(client, namespace); - let pods = pod_api.list(&list_params).await?; - - if pods.items.is_empty() { + if pods.is_empty() { return Err(KubeApiError::invalid_state( &resource, - "no pods matching labels found", + "no pods matching the labels were found", )); } - pods.items - .iter() + pods.iter() .filter_map(|pod| RuntimeData::from_pod(pod, self.container()).ok()) .next() .ok_or_else(|| { KubeApiError::invalid_state( &resource, - "no pod matching labels is ready to be targeted", + "no pod matching the labels is ready to be targeted", ) }) } @@ -344,6 +373,7 @@ impl RuntimeDataProvider for Target { Target::Job(target) => target.runtime_data(client, namespace).await, 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::Targetless => Err(KubeApiError::MissingRuntimeData), } } @@ -358,6 +388,7 @@ impl RuntimeDataProvider for ResolvedTarget { Self::Job(target) => target.runtime_data(client, namespace).await, 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::Targetless(_) => Err(KubeApiError::MissingRuntimeData), } } @@ -365,7 +396,9 @@ impl RuntimeDataProvider for ResolvedTarget { #[cfg(test)] mod tests { - use mirrord_config::target::{deployment::DeploymentTarget, job::JobTarget, pod::PodTarget}; + use mirrord_config::target::{ + deployment::DeploymentTarget, job::JobTarget, pod::PodTarget, service::ServiceTarget, + }; use rstest::rstest; use super::*; @@ -378,6 +411,8 @@ mod tests { #[case("deployment/nginx-deployment/container/container-name", Target::Deployment(DeploymentTarget {deployment: "nginx-deployment".to_string(), container: Some("container-name".to_string())}))] #[case("job/foo", Target::Job(JobTarget { job: "foo".to_string(), container: None }))] #[case("job/foo/container/baz", Target::Job(JobTarget { job: "foo".to_string(), container: Some("baz".to_string()) }))] + #[case("service/foo", Target::Service(ServiceTarget { service: "foo".into(), container: None }))] + #[case("service/foo/container/baz", Target::Service(ServiceTarget { service: "foo".into(), container: Some("baz".into()) }))] fn target_parses(#[case] target: &str, #[case] expected: Target) { let target = target.parse::().unwrap(); assert_eq!(target, expected) diff --git a/mirrord/kube/src/api/runtime/cron_job.rs b/mirrord/kube/src/api/runtime/cron_job.rs index 31b1f09f1b2..0c4dc16e3c9 100644 --- a/mirrord/kube/src/api/runtime/cron_job.rs +++ b/mirrord/kube/src/api/runtime/cron_job.rs @@ -17,9 +17,7 @@ impl RuntimeDataFromLabels for CronJobTarget { self.container.as_deref() } - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .as_ref() diff --git a/mirrord/kube/src/api/runtime/deployment.rs b/mirrord/kube/src/api/runtime/deployment.rs index 11c3383651a..5dab483cad5 100644 --- a/mirrord/kube/src/api/runtime/deployment.rs +++ b/mirrord/kube/src/api/runtime/deployment.rs @@ -17,9 +17,7 @@ impl RuntimeDataFromLabels for DeploymentTarget { self.container.as_deref() } - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .as_ref() diff --git a/mirrord/kube/src/api/runtime/job.rs b/mirrord/kube/src/api/runtime/job.rs index 2bd06af576e..9d75e6afb88 100644 --- a/mirrord/kube/src/api/runtime/job.rs +++ b/mirrord/kube/src/api/runtime/job.rs @@ -17,9 +17,7 @@ impl RuntimeDataFromLabels for JobTarget { self.container.as_deref() } - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .as_ref() diff --git a/mirrord/kube/src/api/runtime/rollout.rs b/mirrord/kube/src/api/runtime/rollout.rs index ee556117dba..d75963ce974 100644 --- a/mirrord/kube/src/api/runtime/rollout.rs +++ b/mirrord/kube/src/api/runtime/rollout.rs @@ -20,9 +20,7 @@ impl RuntimeDataFromLabels for RolloutTarget { } /// Digs into `resource` to return its `.spec.selector.matchLabels`. - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .clone() diff --git a/mirrord/kube/src/api/runtime/service.rs b/mirrord/kube/src/api/runtime/service.rs new file mode 100644 index 00000000000..4b18cdcf648 --- /dev/null +++ b/mirrord/kube/src/api/runtime/service.rs @@ -0,0 +1,27 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use k8s_openapi::api::core::v1::Service; +use mirrord_config::target::service::ServiceTarget; + +use super::RuntimeDataFromLabels; +use crate::error::{KubeApiError, Result}; + +impl RuntimeDataFromLabels for ServiceTarget { + type Resource = Service; + + fn name(&self) -> Cow { + Cow::from(&self.service) + } + + 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.clone()) + .ok_or_else(|| KubeApiError::missing_field(resource, ".spec.selector")) + } +} diff --git a/mirrord/kube/src/api/runtime/stateful_set.rs b/mirrord/kube/src/api/runtime/stateful_set.rs index eae8846af5a..5bb70dbf09c 100644 --- a/mirrord/kube/src/api/runtime/stateful_set.rs +++ b/mirrord/kube/src/api/runtime/stateful_set.rs @@ -17,9 +17,7 @@ impl RuntimeDataFromLabels for StatefulSetTarget { self.container.as_deref() } - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .as_ref() diff --git a/mirrord/kube/src/resolved.rs b/mirrord/kube/src/resolved.rs index 943198fdf9c..70ba34d7535 100644 --- a/mirrord/kube/src/resolved.rs +++ b/mirrord/kube/src/resolved.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use k8s_openapi::api::{ apps::v1::{Deployment, StatefulSet}, batch::v1::{CronJob, Job}, - core::v1::Pod, + core::v1::{Pod, Service}, }; use kube::{Client, Resource, ResourceExt}; use mirrord_config::{feature::network::incoming::ConcurrentSteal, target::Target}; @@ -20,6 +20,7 @@ pub mod deployment; pub mod job; pub mod pod; pub mod rollout; +pub mod service; pub mod stateful_set; /// Helper struct for resolving user-provided [`Target`] to Kubernetes resources. @@ -30,7 +31,7 @@ pub mod stateful_set; /// 1. A generic implementation with helper methods for getting strings such as names, types and so /// on; /// 2. `CHECKED = false` that may be used to build the struct, and to call -/// `assert_valid_mirrord_target` (along with the generic methods); +/// [`ResolvedTarget::assert_valid_mirrord_target`] (along with the generic methods); /// 3. `CHECKED = true` which is how we get a connection url for the target; #[derive(Debug, Clone)] pub enum ResolvedTarget { @@ -39,13 +40,17 @@ pub enum ResolvedTarget { Job(ResolvedResource), CronJob(ResolvedResource), StatefulSet(ResolvedResource), + Service(ResolvedResource), /// [`Pod`] is a special case, in that it does not implement [`RuntimeDataFromLabels`], /// and instead we implement a `runtime_data` method directly in its /// [`ResolvedResource`] impl. Pod(ResolvedResource), - /// Holds the `namespace` for this target. - Targetless(String), + + Targetless( + /// Agent pod's namespace. + String, + ), } /// A kubernetes [`Resource`], and container pair to be used based on the target we @@ -84,6 +89,9 @@ impl ResolvedTarget { ResolvedTarget::StatefulSet(ResolvedResource { resource, .. }) => { resource.metadata.name.as_deref() } + ResolvedTarget::Service(ResolvedResource { resource, .. }) => { + resource.metadata.name.as_deref() + } ResolvedTarget::Targetless(_) => None, } } @@ -96,6 +104,7 @@ impl ResolvedTarget { ResolvedTarget::Job(ResolvedResource { resource, .. }) => resource.name_any(), ResolvedTarget::CronJob(ResolvedResource { resource, .. }) => resource.name_any(), ResolvedTarget::StatefulSet(ResolvedResource { resource, .. }) => resource.name_any(), + ResolvedTarget::Service(ResolvedResource { resource, .. }) => resource.name_any(), ResolvedTarget::Targetless(..) => "targetless".to_string(), } } @@ -120,6 +129,9 @@ impl ResolvedTarget { ResolvedTarget::StatefulSet(ResolvedResource { resource, .. }) => { resource.metadata.namespace.as_deref() } + ResolvedTarget::Service(ResolvedResource { resource, .. }) => { + resource.metadata.namespace.as_deref() + } ResolvedTarget::Targetless(namespace) => Some(namespace), } } @@ -137,6 +149,7 @@ impl ResolvedTarget { ResolvedTarget::StatefulSet(ResolvedResource { resource, .. }) => { resource.metadata.labels } + ResolvedTarget::Service(ResolvedResource { resource, .. }) => resource.metadata.labels, ResolvedTarget::Targetless(_) => None, } } @@ -149,22 +162,11 @@ impl ResolvedTarget { ResolvedTarget::Job(_) => "job", ResolvedTarget::CronJob(_) => "cronjob", ResolvedTarget::StatefulSet(_) => "statefulset", + ResolvedTarget::Service(_) => "service", ResolvedTarget::Targetless(_) => "targetless", } } - pub fn get_container(&self) -> Option<&str> { - match self { - ResolvedTarget::Deployment(ResolvedResource { container, .. }) - | ResolvedTarget::Rollout(ResolvedResource { container, .. }) - | ResolvedTarget::Job(ResolvedResource { container, .. }) - | ResolvedTarget::CronJob(ResolvedResource { container, .. }) - | ResolvedTarget::StatefulSet(ResolvedResource { container, .. }) - | ResolvedTarget::Pod(ResolvedResource { container, .. }) => container.as_deref(), - ResolvedTarget::Targetless(..) => None, - } - } - /// Convenient way of getting the container from this target. pub fn container(&self) -> Option<&str> { match self { @@ -173,6 +175,7 @@ impl ResolvedTarget { | ResolvedTarget::Job(ResolvedResource { container, .. }) | ResolvedTarget::CronJob(ResolvedResource { container, .. }) | ResolvedTarget::StatefulSet(ResolvedResource { container, .. }) + | ResolvedTarget::Service(ResolvedResource { container, .. }) | ResolvedTarget::Pod(ResolvedResource { container, .. }) => container.as_deref(), ResolvedTarget::Targetless(..) => None, } @@ -190,45 +193,6 @@ impl ResolvedTarget { false } } - - /// Returns the number of containers for this [`ResolvedTarget`], defaulting to 1. - pub fn containers_status(&self) -> usize { - match self { - ResolvedTarget::Deployment(ResolvedResource { resource, .. }) => resource - .spec - .as_ref() - .and_then(|spec| spec.template.spec.as_ref()) - .map(|pod_spec| pod_spec.containers.len()), - ResolvedTarget::Rollout(ResolvedResource { resource, .. }) => resource - .spec - .as_ref() - .and_then(|spec| spec.template.as_ref()) - .and_then(|pod_template| pod_template.spec.as_ref()) - .map(|pod_spec| pod_spec.containers.len()), - ResolvedTarget::StatefulSet(ResolvedResource { resource, .. }) => resource - .spec - .as_ref() - .and_then(|spec| spec.template.spec.as_ref()) - .map(|pod_spec| pod_spec.containers.len()), - ResolvedTarget::CronJob(ResolvedResource { resource, .. }) => resource - .spec - .as_ref() - .and_then(|spec| spec.job_template.spec.as_ref()) - .and_then(|job_spec| job_spec.template.spec.as_ref()) - .map(|pod_spec| pod_spec.containers.len()), - ResolvedTarget::Job(ResolvedResource { resource, .. }) => resource - .spec - .as_ref() - .and_then(|spec| spec.template.spec.as_ref()) - .map(|pod_spec| pod_spec.containers.len()), - ResolvedTarget::Pod(ResolvedResource { resource, .. }) => resource - .spec - .as_ref() - .map(|pod_spec| pod_spec.containers.len()), - ResolvedTarget::Targetless(..) => Some(1), - } - .unwrap_or(1) - } } impl ResolvedTarget { @@ -295,6 +259,15 @@ impl ResolvedTarget { container: target.container.clone(), }) }), + Target::Service(target) => get_k8s_resource_api::(client, namespace) + .get(&target.service) + .await + .map(|resource| { + ResolvedTarget::Service(ResolvedResource { + resource, + container: target.container.clone(), + }) + }), Target::Targetless => Ok(ResolvedTarget::Targetless( namespace.unwrap_or("default").to_string(), )), @@ -303,13 +276,20 @@ impl ResolvedTarget { Ok(target) } - /// Check if the target can be used as a mirrord target. + /// Checks if the target can be used via the mirrord Operator. + /// + /// This is implemented in the CLI only to improve the UX (skip roundtrip to the operator). /// - /// 1. [`ResolvedTarget::Deployment`] or [`ResolvedTarget::Rollout`] - has available replicas - /// and the target container, if specified, is found in the spec + /// Performs only basic checks: + /// 1. [`ResolvedTarget::Deployment`], [`ResolvedTarget::Rollout`], + /// [`ResolvedTarget::StatefulSet`] - has available replicas and the target container, if + /// specified, is found in the spec /// 2. [`ResolvedTarget::Pod`] - passes target-readiness check, see [`RuntimeData::from_pod`]. - /// 3. [`ResolvedTarget::Job`] - error, as this is `copy_target` exclusive - /// 4. [`ResolvedTarget::Targetless`] - no check + /// 3. [`ResolvedTarget::Job`] and [`ResolvedTarget::CronJob`] - error, as this is `copy_target` + /// exclusive + /// 4. [`ResolvedTarget::Targetless`] - no check (not applicable) + /// 5. [`ResolvedTarget::Service`] - has available replicas and the target container, if + /// specified, is found in at least one of them #[tracing::instrument(level = Level::DEBUG, skip(client), ret, err)] pub async fn assert_valid_mirrord_target( self, @@ -355,6 +335,7 @@ impl ResolvedTarget { container, })) } + ResolvedTarget::Pod(ResolvedResource { resource, container, @@ -404,9 +385,11 @@ impl ResolvedTarget { ResolvedTarget::Job(..) => { return Err(KubeApiError::requires_copy::()); } + ResolvedTarget::CronJob(..) => { return Err(KubeApiError::requires_copy::()); } + ResolvedTarget::StatefulSet(ResolvedResource { resource, container, @@ -447,6 +430,39 @@ impl ResolvedTarget { })) } + ResolvedTarget::Service(ResolvedResource { + resource, + container, + }) => { + let pods = ResolvedResource::::get_pods(&resource, client).await?; + + if pods.is_empty() { + return Err(KubeApiError::invalid_state( + &resource, + "no pods matching the labels were found", + )); + } + + if let Some(container) = &container { + let exists_in_a_pod = pods + .iter() + .flat_map(|pod| pod.spec.as_ref()) + .flat_map(|spec| &spec.containers) + .any(|found_container| found_container.name == *container); + if !exists_in_a_pod { + return Err(KubeApiError::invalid_state( + &resource, + format_args!("none of the pods that match the labels contain the target container `{container}`" + ))); + } + } + + Ok(ResolvedTarget::Service(ResolvedResource { + resource, + container, + })) + } + ResolvedTarget::Targetless(namespace) => { // no check needed here Ok(ResolvedTarget::Targetless(namespace)) diff --git a/mirrord/kube/src/resolved/cron_job.rs b/mirrord/kube/src/resolved/cron_job.rs index 9fcf4fd5027..60ecabc862e 100644 --- a/mirrord/kube/src/resolved/cron_job.rs +++ b/mirrord/kube/src/resolved/cron_job.rs @@ -24,9 +24,7 @@ impl RuntimeDataFromLabels for ResolvedResource { self.container.as_deref() } - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .as_ref() diff --git a/mirrord/kube/src/resolved/deployment.rs b/mirrord/kube/src/resolved/deployment.rs index af06682c4f2..f85334a3d71 100644 --- a/mirrord/kube/src/resolved/deployment.rs +++ b/mirrord/kube/src/resolved/deployment.rs @@ -21,7 +21,7 @@ impl RuntimeDataFromLabels for ResolvedResource { self.container.as_deref() } - async fn get_selector_match_labels( + fn get_selector_match_labels( resource: &Self::Resource, ) -> Result, KubeApiError> { resource diff --git a/mirrord/kube/src/resolved/job.rs b/mirrord/kube/src/resolved/job.rs index 039d3afec30..28555431d0c 100644 --- a/mirrord/kube/src/resolved/job.rs +++ b/mirrord/kube/src/resolved/job.rs @@ -24,9 +24,7 @@ impl RuntimeDataFromLabels for ResolvedResource { self.container.as_deref() } - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .as_ref() diff --git a/mirrord/kube/src/resolved/rollout.rs b/mirrord/kube/src/resolved/rollout.rs index 4f7ab615c8f..5411b02d5a3 100644 --- a/mirrord/kube/src/resolved/rollout.rs +++ b/mirrord/kube/src/resolved/rollout.rs @@ -22,9 +22,7 @@ impl RuntimeDataFromLabels for ResolvedResource { self.container.as_deref() } - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .as_ref() diff --git a/mirrord/kube/src/resolved/service.rs b/mirrord/kube/src/resolved/service.rs new file mode 100644 index 00000000000..1e7427de74f --- /dev/null +++ b/mirrord/kube/src/resolved/service.rs @@ -0,0 +1,31 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use k8s_openapi::api::core::v1::Service; + +use super::{ResolvedResource, RuntimeDataFromLabels}; +use crate::error::{KubeApiError, Result}; + +impl RuntimeDataFromLabels for ResolvedResource { + type Resource = Service; + + 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.clone()) + .ok_or_else(|| KubeApiError::missing_field(resource, ".spec.selector")) + } +} diff --git a/mirrord/kube/src/resolved/stateful_set.rs b/mirrord/kube/src/resolved/stateful_set.rs index ccc0edeb7a1..aebe74ba317 100644 --- a/mirrord/kube/src/resolved/stateful_set.rs +++ b/mirrord/kube/src/resolved/stateful_set.rs @@ -21,9 +21,7 @@ impl RuntimeDataFromLabels for ResolvedResource { self.container.as_deref() } - async fn get_selector_match_labels( - resource: &Self::Resource, - ) -> Result> { + fn get_selector_match_labels(resource: &Self::Resource) -> Result> { resource .spec .as_ref() diff --git a/mirrord/operator/src/client.rs b/mirrord/operator/src/client.rs index b92b3b1ea97..9550ddfe4b2 100644 --- a/mirrord/operator/src/client.rs +++ b/mirrord/operator/src/client.rs @@ -615,6 +615,8 @@ impl OperatorApi { // `targetless` has no `RuntimeData`! if matches!(target, ResolvedTarget::Targetless(_)).not() { + // Extracting runtime data asserts that the user can see at least one pod from the + // workload/service targets. let runtime_data = target .runtime_data(self.client(), target.namespace()) .await?; diff --git a/mirrord/operator/src/crd.rs b/mirrord/operator/src/crd.rs index b6c3ddd334b..efae6aa6751 100644 --- a/mirrord/operator/src/crd.rs +++ b/mirrord/operator/src/crd.rs @@ -63,6 +63,7 @@ impl TargetCrd { Target::Job(target) => ("job", &target.job, &target.container), 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::Targetless => return TARGETLESS_TARGET_NAME.to_string(), }; diff --git a/mirrord/operator/src/setup.rs b/mirrord/operator/src/setup.rs index 18edfa63c45..80c09c0b36c 100644 --- a/mirrord/operator/src/setup.rs +++ b/mirrord/operator/src/setup.rs @@ -544,6 +544,7 @@ impl OperatorClusterRole { "cronjobs".to_owned(), "statefulsets".to_owned(), "statefulsets/scale".to_owned(), + "services".to_owned(), ]), verbs: vec!["get".to_owned(), "list".to_owned(), "watch".to_owned()], ..Default::default()