-Although that a client for gRPC server is not required for the product,
+Although a client for gRPC server is not required for the product,
it is important for testing and troubleshooting. The client
allows you to manage storage pools and replicas and just use `--help`
-option if not sure how to use it. CSI services are not covered by the client.
+option if you are not sure how to use it. CSI services are not covered by the client.
In following example of a client session is assumed that mayastor has been
diff --git a/composer/Cargo.toml b/composer/Cargo.toml
new file mode 100644
index 000000000..315795f46
--- /dev/null
+++ b/composer/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "composer"
+version = "0.1.0"
+authors = ["Tiago Castro "]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+tokio = { version = "0.2", features = ["full"] }
+futures = "0.3.8"
+tonic = "0.1"
+crossbeam = "0.7.3"
+rpc = { path = "../rpc" }
+ipnetwork = "0.17.0"
+bollard = "0.8.0"
+
+[dev-dependencies]
+tokio = { version = "0.2", features = ["full"] }
diff --git a/composer/src/lib.rs b/composer/src/lib.rs
new file mode 100644
index 000000000..b61db5dd4
--- /dev/null
+++ b/composer/src/lib.rs
@@ -0,0 +1,847 @@
+use std::{
+ collections::HashMap,
+ net::{Ipv4Addr, SocketAddr, TcpStream},
+ thread,
+ time::Duration,
+};
+
+use bollard::{
+ container::{
+ Config,
+ CreateContainerOptions,
+ ListContainersOptions,
+ LogsOptions,
+ NetworkingConfig,
+ RemoveContainerOptions,
+ StopContainerOptions,
+ },
+ errors::Error,
+ network::{CreateNetworkOptions, ListNetworksOptions},
+ service::{
+ ContainerSummaryInner,
+ EndpointIpamConfig,
+ EndpointSettings,
+ HostConfig,
+ Ipam,
+ Mount,
+ MountTypeEnum,
+ Network,
+ PortMap,
+ },
+ Docker,
+};
+use futures::TryStreamExt;
+use ipnetwork::Ipv4Network;
+use tonic::transport::Channel;
+
+use bollard::models::ContainerInspectResponse;
+use rpc::mayastor::{
+ bdev_rpc_client::BdevRpcClient,
+ mayastor_client::MayastorClient,
+};
+pub const TEST_NET_NAME: &str = "mayastor-testing-network";
+pub const TEST_NET_NETWORK: &str = "10.1.0.0/16";
+#[derive(Clone)]
+pub struct RpcHandle {
+ pub name: String,
+ pub endpoint: SocketAddr,
+ pub mayastor: MayastorClient,
+ pub bdev: BdevRpcClient,
+}
+
+impl RpcHandle {
+ /// connect to the containers and construct a handle
+ async fn connect(
+ name: String,
+ endpoint: SocketAddr,
+ ) -> Result {
+ let mut attempts = 60;
+ loop {
+ if TcpStream::connect_timeout(&endpoint, Duration::from_millis(100))
+ .is_ok()
+ {
+ break;
+ } else {
+ thread::sleep(Duration::from_millis(101));
+ }
+ attempts -= 1;
+ if attempts == 0 {
+ return Err(format!(
+ "Failed to connect to {}/{}",
+ name, endpoint
+ ));
+ }
+ }
+
+ let mayastor =
+ MayastorClient::connect(format!("http://{}", endpoint.to_string()))
+ .await
+ .unwrap();
+ let bdev =
+ BdevRpcClient::connect(format!("http://{}", endpoint.to_string()))
+ .await
+ .unwrap();
+
+ Ok(Self {
+ name,
+ mayastor,
+ bdev,
+ endpoint,
+ })
+ }
+}
+
+/// Path to local binary and arguments
+#[derive(Default, Clone)]
+pub struct Binary {
+ path: String,
+ arguments: Vec,
+}
+
+impl Binary {
+ /// Setup local binary from target debug and arguments
+ pub fn from_dbg(name: &str) -> Self {
+ let path = std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"));
+ let srcdir = path.parent().unwrap().to_string_lossy();
+
+ Self::new(format!("{}/target/debug/{}", srcdir, name), vec![])
+ }
+ /// Setup nix shell binary from path and arguments
+ pub fn from_nix(name: &str) -> Self {
+ Self::new(Self::which(name).expect("binary should exist"), vec![])
+ }
+ /// Add single argument
+ /// Only one argument can be passed per use. So instead of:
+ ///
+ /// # Self::from_dbg("hello")
+ /// .with_arg("-n nats")
+ /// # ;
+ ///
+ /// usage would be:
+ ///
+ /// # Self::from_dbg("hello")
+ /// .with_arg("-n")
+ /// .with_arg("nats")
+ /// # ;
+ pub fn with_arg(mut self, arg: &str) -> Self {
+ self.arguments.push(arg.into());
+ self
+ }
+ /// Add multiple arguments via a vector
+ pub fn with_args>(mut self, mut args: Vec) -> Self {
+ self.arguments.extend(args.drain(..).map(|s| s.into()));
+ self
+ }
+
+ fn which(name: &str) -> std::io::Result {
+ let output = std::process::Command::new("which").arg(name).output()?;
+ Ok(String::from_utf8_lossy(&output.stdout).trim().into())
+ }
+ fn new(path: String, args: Vec) -> Self {
+ Self {
+ path,
+ arguments: args,
+ }
+ }
+}
+
+impl Into> for Binary {
+ fn into(self) -> Vec {
+ let mut v = vec![self.path.clone()];
+ v.extend(self.arguments);
+ v
+ }
+}
+
+/// Specs of the allowed containers include only the binary path
+/// (relative to src) and the required arguments
+#[derive(Default, Clone)]
+pub struct ContainerSpec {
+ /// Name of the container
+ name: ContainerName,
+ /// Binary configuration
+ binary: Binary,
+ /// Port mapping to host ports
+ port_map: Option,
+ /// Use Init container
+ init: Option,
+ /// Key-Map of environment variables
+ /// Starts with RUST_LOG=debug,h2=info
+ env: HashMap,
+}
+
+impl Into> for &ContainerSpec {
+ fn into(self) -> Vec {
+ self.binary.clone().into()
+ }
+}
+
+impl ContainerSpec {
+ /// Create new ContainerSpec from name and binary
+ pub fn new(name: &str, binary: Binary) -> Self {
+ let mut env = HashMap::new();
+ env.insert("RUST_LOG".to_string(), "debug,h2=info".to_string());
+ Self {
+ name: name.into(),
+ binary,
+ init: Some(true),
+ env,
+ ..Default::default()
+ }
+ }
+ /// Add port mapping from container to host
+ pub fn with_portmap(mut self, from: &str, to: &str) -> Self {
+ let from = format!("{}/tcp", from);
+ let mut port_map = bollard::service::PortMap::new();
+ let binding = bollard::service::PortBinding {
+ host_ip: None,
+ host_port: Some(to.into()),
+ };
+ port_map.insert(from, Some(vec![binding]));
+ self.port_map = Some(port_map);
+ self
+ }
+ /// Add environment key-val, eg for setting the RUST_LOG
+ /// If a key already exists, the value is replaced
+ pub fn with_env(mut self, key: &str, val: &str) -> Self {
+ if let Some(old) = self.env.insert(key.into(), val.into()) {
+ println!("Replaced key {} val {} with val {}", key, old, val);
+ }
+ self
+ }
+ fn env_to_vec(&self) -> Vec {
+ let mut vec = vec![];
+ self.env.iter().for_each(|(k, v)| {
+ vec.push(format!("{}={}", k, v));
+ });
+ vec
+ }
+}
+
+pub struct Builder {
+ /// name of the experiment this name will be used as a network and labels
+ /// this way we can "group" all objects within docker to match this test
+ /// test. It is highly recommend you use a sane name for this as it will
+ /// help you during debugging
+ name: String,
+ /// containers we want to create, note these are mayastor containers
+ /// only
+ containers: Vec,
+ /// the network for the tests used
+ network: String,
+ /// delete the container and network when dropped
+ clean: bool,
+ /// destroy existing containers if any
+ prune: bool,
+}
+
+impl Default for Builder {
+ fn default() -> Self {
+ Builder::new()
+ }
+}
+
+impl Builder {
+ /// construct a new builder for `[ComposeTest']
+ pub fn new() -> Self {
+ Self {
+ name: "".to_string(),
+ containers: Default::default(),
+ network: "10.1.0.0".to_string(),
+ clean: true,
+ prune: true,
+ }
+ }
+
+ /// set the network for this test
+ pub fn network(mut self, network: &str) -> Builder {
+ self.network = network.to_owned();
+ self
+ }
+
+ /// the name to be used as labels and network name
+ pub fn name(mut self, name: &str) -> Builder {
+ self.name = name.to_owned();
+ self
+ }
+
+ /// add a mayastor container with a name
+ pub fn add_container(mut self, name: &str) -> Builder {
+ self.containers
+ .push(ContainerSpec::new(name, Binary::from_dbg("mayastor")));
+ self
+ }
+
+ /// add a generic container which runs a local binary
+ pub fn add_container_spec(mut self, spec: ContainerSpec) -> Builder {
+ self.containers.push(spec);
+ self
+ }
+
+ /// add a generic container which runs a local binary
+ pub fn add_container_bin(self, name: &str, bin: Binary) -> Builder {
+ self.add_container_spec(ContainerSpec::new(name, bin))
+ }
+
+ /// clean on drop?
+ pub fn with_clean(mut self, enable: bool) -> Builder {
+ self.clean = enable;
+ self
+ }
+
+ pub fn with_prune(mut self, enable: bool) -> Builder {
+ self.prune = enable;
+ self
+ }
+ /// build the config and start the containers
+ pub async fn build(
+ self,
+ ) -> Result> {
+ let mut compose = self.build_only().await?;
+ compose.start_all().await?;
+ Ok(compose)
+ }
+
+ /// build the config but don't start the containers
+ pub async fn build_only(
+ self,
+ ) -> Result> {
+ let net: Ipv4Network = self.network.parse()?;
+
+ let path = std::path::PathBuf::from(std::env!("CARGO_MANIFEST_DIR"));
+ let srcdir = path.parent().unwrap().to_string_lossy().into();
+ let docker = Docker::connect_with_unix_defaults()?;
+
+ let mut cfg = HashMap::new();
+ cfg.insert(
+ "Subnet".to_string(),
+ format!("{}/{}", net.network().to_string(), net.prefix()),
+ );
+ cfg.insert("Gateway".into(), net.nth(1).unwrap().to_string());
+
+ let ipam = Ipam {
+ driver: Some("default".into()),
+ config: Some(vec![cfg]),
+ options: None,
+ };
+
+ let mut compose = ComposeTest {
+ name: self.name.clone(),
+ srcdir,
+ docker,
+ network_id: "".to_string(),
+ containers: Default::default(),
+ ipam,
+ label_prefix: "io.mayastor.test".to_string(),
+ clean: self.clean,
+ prune: self.prune,
+ };
+
+ compose.network_id =
+ compose.network_create().await.map_err(|e| e.to_string())?;
+
+ // containers are created where the IPs are ordinal
+ for (i, spec) in self.containers.iter().enumerate() {
+ compose
+ .create_container(
+ spec,
+ &net.nth((i + 2) as u32).unwrap().to_string(),
+ )
+ .await?;
+ }
+
+ Ok(compose)
+ }
+}
+
+///
+/// Some types to avoid confusion when
+///
+/// different networks are referred to, internally as networkId in docker
+type NetworkId = String;
+/// container name
+type ContainerName = String;
+/// container ID
+type ContainerId = String;
+
+#[derive(Clone, Debug)]
+pub struct ComposeTest {
+ /// used as the network name
+ name: String,
+ /// the source dir the tests are run in
+ srcdir: String,
+ /// handle to the docker daemon
+ docker: Docker,
+ /// the network id is used to attach containers to networks
+ network_id: NetworkId,
+ /// the name of containers and their (IDs, Ipv4) we have created
+ /// perhaps not an ideal data structure, but we can improve it later
+ /// if we need to
+ containers: HashMap,
+ /// the default network configuration we use for our test cases
+ ipam: Ipam,
+ /// prefix for labels set on containers and networks
+ /// $prefix.name = $name will be created automatically
+ label_prefix: String,
+ /// automatically clean up the things we have created for this test
+ clean: bool,
+ pub prune: bool,
+}
+
+impl Drop for ComposeTest {
+ /// destroy the containers and network. Notice that we use sync code here
+ fn drop(&mut self) {
+ if self.clean {
+ self.containers.keys().for_each(|c| {
+ std::process::Command::new("docker")
+ .args(&["stop", c])
+ .output()
+ .unwrap();
+ std::process::Command::new("docker")
+ .args(&["rm", c])
+ .output()
+ .unwrap();
+ });
+
+ std::process::Command::new("docker")
+ .args(&["network", "rm", &self.name])
+ .output()
+ .unwrap();
+ }
+ }
+}
+
+impl ComposeTest {
+ /// Create a new network, with default settings. If a network with the same
+ /// name already exists it will be reused. Note that we do not check the
+ /// networking IP and/or subnets
+ async fn network_create(&mut self) -> Result {
+ let mut net = self.network_list().await?;
+
+ if !net.is_empty() {
+ let first = net.pop().unwrap();
+ self.network_id = first.id.unwrap();
+ return Ok(self.network_id.clone());
+ }
+
+ let name_label = format!("{}.name", self.label_prefix);
+ // we use the same network everywhere
+ let create_opts = CreateNetworkOptions {
+ name: TEST_NET_NAME,
+ check_duplicate: true,
+ driver: "bridge",
+ internal: false,
+ attachable: true,
+ ingress: false,
+ ipam: self.ipam.clone(),
+ enable_ipv6: false,
+ options: vec![("com.docker.network.bridge.name", "mayabridge0")]
+ .into_iter()
+ .collect(),
+ labels: vec![(name_label.as_str(), self.name.as_str())]
+ .into_iter()
+ .collect(),
+ };
+
+ self.docker.create_network(create_opts).await.map(|r| {
+ self.network_id = r.id.unwrap();
+ self.network_id.clone()
+ })
+ }
+
+ async fn network_remove(&self) -> Result<(), Error> {
+ // if the network is not found, its not an error, any other error is
+ // reported as such. Networks can only be destroyed when all containers
+ // attached to it are removed. To get a list of attached
+ // containers, use network_list()
+ if let Err(e) = self.docker.remove_network(&self.name).await {
+ if !matches!(e, Error::DockerResponseNotFoundError{..}) {
+ return Err(e);
+ }
+ }
+
+ Ok(())
+ }
+
+ /// list all the docker networks
+ pub async fn network_list(&self) -> Result, Error> {
+ self.docker
+ .list_networks(Some(ListNetworksOptions {
+ filters: vec![("name", vec![TEST_NET_NAME])]
+ .into_iter()
+ .collect(),
+ }))
+ .await
+ }
+
+ /// list containers
+ pub async fn list_containers(
+ &self,
+ ) -> Result, Error> {
+ self.docker
+ .list_containers(Some(ListContainersOptions {
+ all: true,
+ filters: vec![(
+ "label",
+ vec![format!("{}.name={}", self.label_prefix, self.name)
+ .as_str()],
+ )]
+ .into_iter()
+ .collect(),
+ ..Default::default()
+ }))
+ .await
+ }
+
+ /// remove a container from the configuration
+ async fn remove_container(&self, name: &str) -> Result<(), Error> {
+ self.docker
+ .remove_container(
+ name,
+ Some(RemoveContainerOptions {
+ v: true,
+ force: true,
+ link: false,
+ }),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ /// remove all containers and its network
+ async fn remove_all(&self) -> Result<(), Error> {
+ for k in &self.containers {
+ self.stop(&k.0).await?;
+ self.remove_container(&k.0).await?;
+ while let Ok(_c) = self.docker.inspect_container(&k.0, None).await {
+ tokio::time::delay_for(Duration::from_millis(500)).await;
+ }
+ }
+ self.network_remove().await?;
+ Ok(())
+ }
+
+ /// we need to construct several objects to create a setup that meets our
+ /// liking:
+ ///
+ /// (1) hostconfig: that configures the host side of the container, i.e what
+ /// features/settings from the host perspective do we want too setup
+ /// for the container. (2) endpoints: this allows us to plugin in the
+ /// container into our network configuration (3) config: the actual
+ /// config which includes the above objects
+ async fn create_container(
+ &mut self,
+ spec: &ContainerSpec,
+ ipv4: &str,
+ ) -> Result<(), Error> {
+ if self.prune {
+ let _ = self
+ .docker
+ .stop_container(
+ &spec.name,
+ Some(StopContainerOptions {
+ t: 0,
+ }),
+ )
+ .await;
+ let _ = self
+ .docker
+ .remove_container(
+ &spec.name,
+ Some(RemoveContainerOptions {
+ v: false,
+ force: false,
+ link: false,
+ }),
+ )
+ .await;
+ }
+
+ let host_config = HostConfig {
+ binds: Some(vec![
+ format!("{}:{}", self.srcdir, self.srcdir),
+ "/nix:/nix:ro".into(),
+ "/dev/hugepages:/dev/hugepages:rw".into(),
+ ]),
+ mounts: Some(vec![
+ // DPDK needs to have a /tmp
+ Mount {
+ target: Some("/tmp".into()),
+ typ: Some(MountTypeEnum::TMPFS),
+ ..Default::default()
+ },
+ // mayastor needs to have a /var/tmp
+ Mount {
+ target: Some("/var/tmp".into()),
+ typ: Some(MountTypeEnum::TMPFS),
+ ..Default::default()
+ },
+ ]),
+ cap_add: Some(vec![
+ "SYS_ADMIN".to_string(),
+ "IPC_LOCK".into(),
+ "SYS_NICE".into(),
+ ]),
+ security_opt: Some(vec!["seccomp:unconfined".into()]),
+ init: spec.init,
+ port_bindings: spec.port_map.clone(),
+ ..Default::default()
+ };
+
+ let mut endpoints_config = HashMap::new();
+ endpoints_config.insert(
+ self.name.as_str(),
+ EndpointSettings {
+ network_id: Some(self.network_id.to_string()),
+ ipam_config: Some(EndpointIpamConfig {
+ ipv4_address: Some(ipv4.into()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ );
+
+ let mut env = spec.env_to_vec();
+ env.push(format!("MY_POD_IP={}", ipv4));
+
+ let cmd: Vec = spec.into();
+ let name = spec.name.as_str();
+
+ // figure out why ports to expose based on the port mapping
+ let mut exposed_ports = HashMap::new();
+ if let Some(map) = spec.port_map.as_ref() {
+ map.iter().for_each(|binding| {
+ exposed_ports.insert(binding.0.as_str(), HashMap::new());
+ })
+ }
+
+ let name_label = format!("{}.name", self.label_prefix);
+ let config = Config {
+ cmd: Some(cmd.iter().map(|s| s.as_str()).collect()),
+ env: Some(env.iter().map(|s| s.as_str()).collect()),
+ image: None, // notice we do not have a base image here
+ hostname: Some(name),
+ host_config: Some(host_config),
+ networking_config: Some(NetworkingConfig {
+ endpoints_config,
+ }),
+ working_dir: Some(self.srcdir.as_str()),
+ volumes: Some(
+ vec![
+ ("/dev/hugepages", HashMap::new()),
+ ("/nix", HashMap::new()),
+ (self.srcdir.as_str(), HashMap::new()),
+ ]
+ .into_iter()
+ .collect(),
+ ),
+ labels: Some(
+ vec![(name_label.as_str(), self.name.as_str())]
+ .into_iter()
+ .collect(),
+ ),
+ exposed_ports: Some(exposed_ports),
+ ..Default::default()
+ };
+
+ let container = self
+ .docker
+ .create_container(
+ Some(CreateContainerOptions {
+ name,
+ }),
+ config,
+ )
+ .await
+ .unwrap();
+
+ self.containers
+ .insert(name.to_string(), (container.id, ipv4.parse().unwrap()));
+
+ Ok(())
+ }
+
+ /// start the container
+ pub async fn start(&self, name: &str) -> Result<(), Error> {
+ let id = self.containers.get(name).unwrap();
+ self.docker
+ .start_container::<&str>(id.0.as_str(), None)
+ .await?;
+
+ Ok(())
+ }
+
+ /// stop the container
+ pub async fn stop(&self, name: &str) -> Result<(), Error> {
+ let id = self.containers.get(name).unwrap();
+ if let Err(e) = self
+ .docker
+ .stop_container(
+ id.0.as_str(),
+ Some(StopContainerOptions {
+ t: 3,
+ }),
+ )
+ .await
+ {
+ // where already stopped
+ if !matches!(e, Error::DockerResponseNotModifiedError{..}) {
+ return Err(e);
+ }
+ }
+
+ Ok(())
+ }
+
+ /// get the logs from the container. It would be nice to make it implicit
+ /// that is, when you make a rpc call, whatever logs where created due to
+ /// that are returned
+ pub async fn logs(&self, name: &str) -> Result<(), Error> {
+ let logs = self
+ .docker
+ .logs(
+ name,
+ Some(LogsOptions {
+ follow: false,
+ stdout: true,
+ stderr: true,
+ since: 0, // TODO log lines since last call?
+ until: 0,
+ timestamps: false,
+ tail: "all",
+ }),
+ )
+ .try_collect::>()
+ .await?;
+
+ logs.iter().for_each(|l| print!("{}:{}", name, l));
+ Ok(())
+ }
+
+ /// get the logs from all of the containers. It would be nice to make it
+ /// implicit that is, when you make a rpc call, whatever logs where
+ /// created due to that are returned
+ pub async fn logs_all(&self) -> Result<(), Error> {
+ for container in &self.containers {
+ let _ = self.logs(&container.0).await;
+ }
+ Ok(())
+ }
+
+ /// start all the containers
+ async fn start_all(&mut self) -> Result<(), Error> {
+ for k in &self.containers {
+ self.start(&k.0).await?;
+ }
+
+ Ok(())
+ }
+
+ /// start the containers
+ pub async fn start_containers(
+ &self,
+ containers: Vec<&str>,
+ ) -> Result<(), Error> {
+ for k in containers {
+ self.start(k).await?;
+ }
+ Ok(())
+ }
+
+ /// inspect the given container
+ pub async fn inspect(
+ &self,
+ name: &str,
+ ) -> Result {
+ self.docker.inspect_container(name, None).await
+ }
+
+ /// pause the container; unfortunately, when the API returns it does not
+ /// mean that the container indeed is frozen completely, in the sense
+ /// that it's not to be assumed that right after a call -- the container
+ /// stops responding.
+ pub async fn pause(&self, name: &str) -> Result<(), Error> {
+ let id = self.containers.get(name).unwrap();
+ self.docker.pause_container(id.0.as_str()).await?;
+
+ Ok(())
+ }
+
+ /// un_pause the container
+ pub async fn thaw(&self, name: &str) -> Result<(), Error> {
+ let id = self.containers.get(name).unwrap();
+ self.docker.unpause_container(id.0.as_str()).await
+ }
+
+ /// return grpc handles to the containers
+ pub async fn grpc_handles(&self) -> Result, String> {
+ let mut handles = Vec::new();
+ for v in &self.containers {
+ handles.push(
+ RpcHandle::connect(
+ v.0.clone(),
+ format!("{}:10124", v.1 .1).parse::().unwrap(),
+ )
+ .await?,
+ );
+ }
+
+ Ok(handles)
+ }
+
+ /// return grpc handle to the container
+ pub async fn grpc_handle(&self, name: &str) -> Result {
+ match self.containers.iter().find(|&c| c.0 == name) {
+ Some(container) => Ok(RpcHandle::connect(
+ container.0.clone(),
+ format!("{}:10124", container.1 .1)
+ .parse::()
+ .unwrap(),
+ )
+ .await?),
+ None => Err(format!("Container {} not found!", name)),
+ }
+ }
+
+ /// explicitly remove all containers
+ pub async fn down(&self) {
+ self.remove_all().await.unwrap();
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use rpc::mayastor::Null;
+
+ #[tokio::test]
+ async fn compose() {
+ let test = Builder::new()
+ .name("composer")
+ .network("10.1.0.0/16")
+ .add_container_spec(
+ ContainerSpec::new(
+ "nats",
+ Binary::from_nix("nats-server").with_arg("-DV"),
+ )
+ .with_portmap("4222", "4222"),
+ )
+ .add_container("mayastor")
+ .add_container_bin(
+ "mayastor2",
+ Binary::from_dbg("mayastor")
+ .with_args(vec!["-n", "nats.composer"]),
+ )
+ .with_clean(true)
+ .build()
+ .await
+ .unwrap();
+
+ let mut hdl = test.grpc_handle("mayastor").await.unwrap();
+ hdl.mayastor.list_nexus(Null {}).await.expect("list nexus");
+
+ // run with --nocapture to get the logs
+ test.logs_all().await.unwrap();
+ }
+}
diff --git a/csi/build.rs b/csi/build.rs
index 78c9462bd..362d8edcd 100644
--- a/csi/build.rs
+++ b/csi/build.rs
@@ -4,5 +4,9 @@ fn main() {
tonic_build::configure()
.build_server(true)
.compile(&["proto/csi.proto"], &["proto"])
- .unwrap_or_else(|e| panic!("csi protobuf compilation failed: {}", e));
+ .expect("csi protobuf compilation failed");
+ tonic_build::configure()
+ .build_server(true)
+ .compile(&["proto/mayastornodeplugin.proto"], &["proto"])
+ .expect("mayastor node grpc service protobuf compilation failed");
}
diff --git a/csi/moac/.gitignore b/csi/moac/.gitignore
index 213915fcf..1ca28f6cc 100644
--- a/csi/moac/.gitignore
+++ b/csi/moac/.gitignore
@@ -1,6 +1,11 @@
/node_modules/
/proto/
/result
-/replica.js
-/pool.js
+/watcher.js
/nexus.js
+/node_operator.js
+/pool.js
+/pool_operator.js
+/replica.js
+/volume_operator.js
+/*.js.map
diff --git a/csi/moac/crds/mayastornode.yaml b/csi/moac/crds/mayastornode.yaml
index 54026ab3e..c3265638e 100644
--- a/csi/moac/crds/mayastornode.yaml
+++ b/csi/moac/crds/mayastornode.yaml
@@ -1,47 +1,50 @@
-apiVersion: apiextensions.k8s.io/v1beta1
+apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mayastornodes.openebs.io
spec:
group: openebs.io
- version: v1alpha1
+ versions:
+ - name: v1alpha1
+ served: true
+ storage: true
+ subresources:
+ # Both status and spec parts are updated by the controller.
+ status: {}
+ schema:
+ openAPIV3Schema:
+ type: object
+ properties:
+ apiVersion:
+ type: string
+ kind:
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: Specification of the mayastor node.
+ type: object
+ required:
+ - grpcEndpoint
+ properties:
+ grpcEndpoint:
+ description: Address of gRPC server that mayastor listens on
+ type: string
+ status:
+ description: State of the node as seen by the control plane
+ type: string
+ additionalPrinterColumns:
+ - name: State
+ type: string
+ description: State of the storage pool
+ jsonPath: .status
+ - name: Age
+ type: date
+ jsonPath: .metadata.creationTimestamp
scope: Namespaced
names:
kind: MayastorNode
listKind: MayastorNodeList
plural: mayastornodes
singular: mayastornode
- shortNames: ['msn']
- additionalPrinterColumns:
- - name: State
- type: string
- description: State of the storage pool
- JSONPath: .status
- - name: Age
- type: date
- JSONPath: .metadata.creationTimestamp
- subresources:
- # Both status and spec parts are updated by the controller.
- status: {}
- validation:
- openAPIV3Schema:
- type: object
- properties:
- apiVersion:
- type: string
- kind:
- type: string
- metadata:
- type: object
- spec:
- description: Specification of the mayastor node.
- type: object
- required:
- - grpcEndpoint
- properties:
- grpcEndpoint:
- description: Address of gRPC server that mayastor listens on
- type: string
- status:
- description: State of the node as seen by the control plane
- type: string
+ shortNames: ['msn']
\ No newline at end of file
diff --git a/csi/moac/crds/mayastorpool.yaml b/csi/moac/crds/mayastorpool.yaml
index 46aa2f2da..bc48b08ae 100644
--- a/csi/moac/crds/mayastorpool.yaml
+++ b/csi/moac/crds/mayastorpool.yaml
@@ -1,10 +1,77 @@
-apiVersion: apiextensions.k8s.io/v1beta1
+apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mayastorpools.openebs.io
spec:
group: openebs.io
- version: v1alpha1
+ versions:
+ - name: v1alpha1
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ schema:
+ openAPIV3Schema:
+ type: object
+ properties:
+ apiVersion:
+ type: string
+ kind:
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: Specification of the mayastor pool.
+ type: object
+ required:
+ - node
+ - disks
+ properties:
+ node:
+ description: Name of the k8s node where the storage pool is located.
+ type: string
+ disks:
+ description: Disk devices (paths or URIs) that should be used for the pool.
+ type: array
+ items:
+ type: string
+ status:
+ description: Status part updated by the pool controller.
+ type: object
+ properties:
+ state:
+ description: Pool state.
+ type: string
+ reason:
+ description: Reason for the pool state value if applicable.
+ type: string
+ disks:
+ description: Disk device URIs that are actually used for the pool.
+ type: array
+ items:
+ type: string
+ capacity:
+ description: Capacity of the pool in bytes.
+ type: integer
+ format: int64
+ minimum: 0
+ used:
+ description: How many bytes are used in the pool.
+ type: integer
+ format: int64
+ minimum: 0
+ additionalPrinterColumns:
+ - name: Node
+ type: string
+ description: Node where the storage pool is located
+ jsonPath: .spec.node
+ - name: State
+ type: string
+ description: State of the storage pool
+ jsonPath: .status.state
+ - name: Age
+ type: date
+ jsonPath: .metadata.creationTimestamp
scope: Namespaced
names:
kind: MayastorPool
@@ -12,67 +79,3 @@ spec:
plural: mayastorpools
singular: mayastorpool
shortNames: ["msp"]
- additionalPrinterColumns:
- - name: Node
- type: string
- description: Node where the storage pool is located
- JSONPath: .spec.node
- - name: State
- type: string
- description: State of the storage pool
- JSONPath: .status.state
- - name: Age
- type: date
- JSONPath: .metadata.creationTimestamp
- subresources:
- status: {}
- validation:
- openAPIV3Schema:
- type: object
- properties:
- apiVersion:
- type: string
- kind:
- type: string
- metadata:
- type: object
- spec:
- description: Specification of the mayastor pool.
- type: object
- required:
- - node
- - disks
- properties:
- node:
- description: Name of the k8s node where the storage pool is located.
- type: string
- disks:
- description: Disk devices (paths or URIs) that should be used for the pool.
- type: array
- items:
- type: string
- status:
- description: Status part updated by the pool controller.
- type: object
- properties:
- state:
- description: Pool state.
- type: string
- reason:
- description: Reason for the pool state value if applicable.
- type: string
- disks:
- description: Disk device URIs that are actually used for the pool.
- type: array
- items:
- type: string
- capacity:
- description: Capacity of the pool in bytes.
- type: integer
- format: int64
- minimum: 0
- used:
- description: How many bytes are used in the pool.
- type: integer
- format: int64
- minimum: 0
diff --git a/csi/moac/crds/mayastorvolume.yaml b/csi/moac/crds/mayastorvolume.yaml
index 074a186a3..a92ab1b8c 100644
--- a/csi/moac/crds/mayastorvolume.yaml
+++ b/csi/moac/crds/mayastorvolume.yaml
@@ -1,138 +1,141 @@
-apiVersion: apiextensions.k8s.io/v1beta1
+apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: mayastorvolumes.openebs.io
spec:
group: openebs.io
- version: v1alpha1
- scope: Namespaced
- names:
- kind: MayastorVolume
- listKind: MayastorVolumeList
- plural: mayastorvolumes
- singular: mayastorvolume
- shortNames: ['msv']
- additionalPrinterColumns:
- - name: Node
- type: string
- description: Node where the volume is located
- JSONPath: .status.node
- - name: Size
- type: integer
- format: int64
- minimum: 0
- description: Size of the volume
- JSONPath: .status.size
- - name: State
- type: string
- description: State of the storage pool
- JSONPath: .status.state
- - name: Age
- type: date
- JSONPath: .metadata.creationTimestamp
- subresources:
- # The status part is updated by the controller and spec part by the user
- # usually. Well, not in this case. The mayastor's control plane updates both
- # parts and user is allowed to update some of the properties in the spec
- # too. The status part is read-only for the user as it is usually done.
- status: {}
- validation:
- openAPIV3Schema:
- type: object
- properties:
- apiVersion:
- type: string
- kind:
- type: string
- metadata:
- type: object
- spec:
- description: Specification of the mayastor volume.
+ versions:
+ - name: v1alpha1
+ served: true
+ storage: true
+ subresources:
+ # The status part is updated by the controller and spec part by the user
+ # usually. Well, not in this case. The mayastor's control plane updates both
+ # parts and user is allowed to update some of the properties in the spec
+ # too. The status part is read-only for the user as it is usually done.
+ status: {}
+ schema:
+ openAPIV3Schema:
type: object
- required:
- - replicaCount
- - requiredBytes
properties:
- replicaCount:
- description: The number of replicas used for the volume.
- type: integer
- minimum: 1
- preferredNodes:
- description: A list of preferred cluster nodes for the volume.
- type: array
- items:
- type: string
- requiredNodes:
- description: Only cluster nodes from this list should be used for the volume.
- type: array
- items:
- type: string
- requiredBytes:
- description: The minimum size of the volume.
- type: integer
- minimum: 1
- limitBytes:
- description: The maximum size of the volume (if zero then same as the requiredBytes).
- type: integer
- minimum: 0
- protocol:
- description: Share protocol of the nexus
+ apiVersion:
type: string
- status:
- description: Properties related to current state of the volume.
- type: object
- properties:
- size:
- description: The size of the volume if it has been created
- type: integer
- format: int64
- state:
- description: Overall state of the volume.
- type: string
- reason:
- description: Further explanation of the state if applicable.
+ kind:
type: string
- node:
- description: Name of the k8s node with the nexus.
- type: string
- nexus:
- description: Frontend of the volume.
+ metadata:
+ type: object
+ spec:
+ description: Specification of the mayastor volume.
type: object
+ required:
+ - replicaCount
+ - requiredBytes
properties:
- deviceUri:
- description: URI of a block device for IO.
+ replicaCount:
+ description: The number of replicas used for the volume.
+ type: integer
+ minimum: 1
+ preferredNodes:
+ description: A list of preferred cluster nodes for the volume.
+ type: array
+ items:
+ type: string
+ requiredNodes:
+ description: Only cluster nodes from this list should be used for the volume.
+ type: array
+ items:
+ type: string
+ requiredBytes:
+ description: The minimum size of the volume.
+ type: integer
+ minimum: 1
+ limitBytes:
+ description: The maximum size of the volume (if zero then same as the requiredBytes).
+ type: integer
+ minimum: 0
+ protocol:
+ description: Share protocol of the nexus
type: string
+ status:
+ description: Properties related to current state of the volume.
+ type: object
+ properties:
+ size:
+ description: The size of the volume if it has been created
+ type: integer
+ format: int64
state:
- description: State of the nexus.
+ description: Overall state of the volume.
+ type: string
+ reason:
+ description: Further explanation of the state if applicable.
+ type: string
+ node:
+ description: Name of the k8s node with the nexus.
type: string
- children:
- description: Child devices of the nexus (replicas).
+ nexus:
+ description: Frontend of the volume.
+ type: object
+ properties:
+ deviceUri:
+ description: URI of a block device for IO.
+ type: string
+ state:
+ description: State of the nexus.
+ type: string
+ children:
+ description: Child devices of the nexus (replicas).
+ type: array
+ items:
+ description: child device of the nexus (replica).
+ type: object
+ properties:
+ uri:
+ description: URI used by nexus to access the child.
+ type: string
+ state:
+ description: State of the child as seen by the nexus.
+ type: string
+ replicas:
+ description: List of replicas
type: array
items:
- description: child device of the nexus (replica).
type: object
properties:
- uri:
- description: URI used by nexus to access the child.
+ node:
+ description: Name of the k8s node with the replica.
type: string
- state:
- description: State of the child as seen by the nexus.
+ pool:
+ description: Name of the pool that replica was created on.
type: string
- replicas:
- description: List of replicas
- type: array
- items:
- type: object
- properties:
- node:
- description: Name of the k8s node with the replica.
- type: string
- pool:
- description: Name of the pool that replica was created on.
- type: string
- uri:
- description: URI of the replica used by the nexus.
- type: string
- offline:
- description: Is replica reachable by control plane.
- type: boolean
+ uri:
+ description: URI of the replica used by the nexus.
+ type: string
+ offline:
+ description: Is replica reachable by control plane.
+ type: boolean
+ additionalPrinterColumns:
+ - name: Node
+ type: string
+ description: Node where the volume is located
+ jsonPath: .status.node
+ - name: Size
+ type: integer
+ format: int64
+ minimum: 0
+ description: Size of the volume
+ jsonPath: .status.size
+ - name: State
+ type: string
+ description: State of the storage pool
+ jsonPath: .status.state
+ - name: Age
+ type: date
+ jsonPath: .metadata.creationTimestamp
+ scope: Namespaced
+ names:
+ kind: MayastorVolume
+ listKind: MayastorVolumeList
+ plural: mayastorvolumes
+ singular: mayastorvolume
+ shortNames: ['msv']
diff --git a/csi/moac/finalizer_helper.ts b/csi/moac/finalizer_helper.ts
deleted file mode 100644
index 4eb89c7da..000000000
--- a/csi/moac/finalizer_helper.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-//
-'use strict';
-
-const k8s = require('@kubernetes/client-node');
-const log = require('./logger').Logger('finalizer_helper');
-
-export class FinalizerHelper {
- private kubeConfig: any;
- private k8sApi: any;
- private namespace: String;
- private groupname: String;
- private version: String;
- private plural: String;
-
- constructor (namespace: String, groupname:String, version:String, plural:String) {
- this.namespace = namespace;
- this.groupname = groupname;
- this.version = version;
- this.kubeConfig = new k8s.KubeConfig();
- this.kubeConfig.loadFromDefault();
- this.k8sApi = this.kubeConfig.makeApiClient(k8s.CustomObjectsApi);
- this.plural = plural;
- }
-
- addFinalizer(body: any, instancename: String, finalizer: String) {
- if (body.metadata.deletionTimestamp != undefined) {
- log.warn(`addFinalizer(${instancename},${finalizer}), deletionTimestamp is set`);
- return;
- }
-
- if (body.metadata.finalizers != undefined) {
- const index = body.metadata.finalizers.indexOf(finalizer);
- if ( index > -1) {
- log.debug(`@addFinalizer(${instancename},${finalizer}), finalizer already present`);
- return;
- }
- body.metadata.finalizers.push(finalizer);
- } else {
- body.metadata.finalizers = [finalizer];
- }
-
- // TODO: use patchNamespacedCustomObject
- this.k8sApi.replaceNamespacedCustomObject(
- this.groupname,
- this.version,
- this.namespace,
- this.plural,
- instancename,
- body)
- .then((res:any) => {
- log.debug(`added finalizer:${finalizer} to ${this.plural}:${instancename}`);
- })
- .catch((err:any) => {
- log.error(`add finalizer:${finalizer} to ${this.plural}:${instancename}, update failed: code=${err.body.code}, reason=${err.body.reason}, ${err.body.message}`);
- });
- }
-
- removeFinalizer(body: any, instancename: String, finalizer: String) {
- if (body.metadata.finalizers == undefined) {
- log.debug(`removeFinalizer(${instancename},${finalizer}), no finalizers defined.`);
- return;
- }
-
- const index = body.metadata.finalizers.indexOf(finalizer);
- if ( index < 0) {
- log.debug(`removeFinalizer(${instancename},${finalizer}), finalizer not found`);
- return;
- }
- body.metadata.finalizers.splice(index, 1);
-
- // TODO: use patchNamespacedCustomObject
- this.k8sApi.replaceNamespacedCustomObject(
- this.groupname,
- this.version,
- this.namespace,
- this.plural,
- instancename,
- body).
- then((res:any) => {
- log.debug(`removed finalizer:${finalizer} from ${this.plural}:${instancename}`);
- })
- .catch((err: any) => {
- log.error(`remove finalizer:${finalizer} from ${this.plural}:${instancename}, update failed: code=${err.body.code}, reason=${err.body.reason}, ${err.body.message}`);
- });
- }
-
- addFinalizerToCR(instancename: String, finalizer: String) {
- this.k8sApi.getNamespacedCustomObject(
- this.groupname,
- this.version,
- this.namespace,
- this.plural,
- instancename)
- .then((customresource:any) => {
- let body = customresource.body;
-
- if (body.metadata.deletionTimestamp != undefined) {
- log.warn(`addFinalizerToCR(${instancename},${finalizer}), deletionTimestamp is set`);
- return;
- }
-
- if (body.metadata.finalizers != undefined) {
- const index = body.metadata.finalizers.indexOf(finalizer);
- if ( index > -1) {
- log.debug(`@addFinalizerToCR(${instancename},${finalizer}), finalizer already present`);
- return;
- }
- body.metadata.finalizers.splice(-1, 0, finalizer);
- } else {
- body.metadata.finalizers = [finalizer];
- }
-
- // TODO: use patchNamespacedCustomObject
- this.k8sApi.replaceNamespacedCustomObject(
- this.groupname,
- this.version,
- this.namespace,
- this.plural,
- instancename,
- body)
- .then((res:any) => {
- log.debug(`added finalizer:${finalizer} to ${this.plural}:${instancename}`);
- })
- .catch((err:any) => {
- log.error(`add finalizer:${finalizer} to ${this.plural}:${instancename}, update failed: code=${err.body.code}, reason=${err.body.reason}, ${err.body.message}`);
- });
- })
- .catch((err: any) => {
- log.error(`add finalizer:${finalizer} to ${this.plural}:${instancename}, get failed: code=${err.body.code}, reason=${err.body.reason}, ${err.body.message}`);
- });
- }
-
- removeFinalizerFromCR(instancename: String, finalizer: String) {
- this.k8sApi.getNamespacedCustomObject(
- this.groupname,
- this.version,
- this.namespace,
- this.plural,
- instancename)
- .then((customresource:any) => {
- let body = customresource.body;
- if (body.metadata.finalizers == undefined) {
- log.debug(`removeFinalizerFromCR(${instancename},${finalizer}), no finalizers on pool`);
- return;
- }
-
- const index = body.metadata.finalizers.indexOf(finalizer);
- if ( index < 0) {
- log.debug(`removeFinalizerFromCR(${instancename},${finalizer}), finalizer not found`);
- return;
- }
- body.metadata.finalizers.splice(index, 1);
-
- // TODO: use patchNamespacedCustomObject
- this.k8sApi.replaceNamespacedCustomObject(
- this.groupname,
- this.version,
- this.namespace,
- this.plural,
- instancename,
- body).
- then((res:any) => {
- log.debug(`removed finalizer:${finalizer} from ${this.plural}:${instancename}`);
- })
- .catch((err: any) => {
- log.error(`remove finalizer:${finalizer} from ${this.plural}:${instancename}, update failed: code=${err.body.code}, reason=${err.body.reason}, ${err.body.message}`);
- });
- })
- .catch((err: any) => {
- log.error(`remove finalizer:${finalizer} from ${this.plural}:${instancename}, get failed: code=${err.body.code}, reason=${err.body.reason}, ${err.body.message}`);
- });
- }
-}
diff --git a/csi/moac/index.js b/csi/moac/index.js
index 9760d516b..58d081fc8 100755
--- a/csi/moac/index.js
+++ b/csi/moac/index.js
@@ -5,54 +5,46 @@
'use strict';
-const { Client, KubeConfig } = require('kubernetes-client');
-const Request = require('kubernetes-client/backends/request');
+const { KubeConfig } = require('client-node-fixed-watcher');
const yargs = require('yargs');
const logger = require('./logger');
const Registry = require('./registry');
-const NodeOperator = require('./node_operator');
-const PoolOperator = require('./pool_operator');
+const { NodeOperator } = require('./node_operator');
+const { PoolOperator } = require('./pool_operator');
const Volumes = require('./volumes');
-const VolumeOperator = require('./volume_operator');
+const { VolumeOperator } = require('./volume_operator');
const ApiServer = require('./rest_api');
const CsiServer = require('./csi').CsiServer;
const { MessageBus } = require('./nats');
const log = new logger.Logger();
-// Read k8s client configuration, in order to be able to connect to k8s api
-// server, either from a file or from environment and return k8s client
-// object.
+// Load k8s config file.
//
// @param {string} [kubefile] Kube config file.
// @returns {object} k8s client object.
-function createK8sClient (kubefile) {
- var backend;
+function createKubeConfig (kubefile) {
+ const kubeConfig = new KubeConfig();
try {
- if (kubefile != null) {
+ if (kubefile) {
log.info('Reading k8s configuration from file ' + kubefile);
- const kubeconfig = new KubeConfig();
- kubeconfig.loadFromFile(kubefile);
- backend = new Request({ kubeconfig });
+ kubeConfig.loadFromFile(kubefile);
+ } else {
+ kubeConfig.loadFromDefault();
}
- return new Client({ backend });
} catch (e) {
log.error('Cannot get k8s client configuration: ' + e);
process.exit(1);
}
+ return kubeConfig;
}
async function main () {
- var client;
- var registry;
- var volumes;
- var poolOper;
- var volumeOper;
- var csiNodeOper;
- var nodeOper;
- var csiServer;
- var apiServer;
- var messageBus;
+ let poolOper;
+ let volumeOper;
+ let csiNodeOper;
+ let nodeOper;
+ let kubeConfig;
const opts = yargs
.options({
@@ -96,6 +88,12 @@ async function main () {
alias: 'verbose',
describe: 'Print debug log messages',
count: true
+ },
+ w: {
+ alias: 'watcher-idle-timeout',
+ describe: 'Restart watcher connections after this many seconds if idle',
+ default: 0,
+ number: true
}
})
.help('help')
@@ -118,13 +116,13 @@ async function main () {
if (csiServer) csiServer.undoReady();
if (apiServer) apiServer.stop();
if (!opts.s) {
- if (volumeOper) await volumeOper.stop();
+ if (volumeOper) volumeOper.stop();
}
if (volumes) volumes.stop();
if (!opts.s) {
- if (poolOper) await poolOper.stop();
+ if (poolOper) poolOper.stop();
if (csiNodeOper) await csiNodeOper.stop();
- if (nodeOper) await nodeOper.stop();
+ if (nodeOper) nodeOper.stop();
}
if (messageBus) messageBus.stop();
if (registry) registry.close();
@@ -142,40 +140,53 @@ async function main () {
// Create csi server before starting lengthy initialization so that we can
// serve csi.identity() calls while getting ready.
- csiServer = new CsiServer(opts.csiAddress);
+ const csiServer = new CsiServer(opts.csiAddress);
await csiServer.start();
- registry = new Registry();
+ const registry = new Registry();
// Listen to register and deregister messages from mayastor nodes
- messageBus = new MessageBus(registry);
+ const messageBus = new MessageBus(registry);
messageBus.start(opts.m);
if (!opts.s) {
// Create k8s client and load openAPI spec from k8s api server
- client = createK8sClient(opts.kubeconfig);
- log.debug('Loading openAPI spec from the server');
- await client.loadSpec();
+ kubeConfig = createKubeConfig(opts.kubeconfig);
// Start k8s operators
- nodeOper = new NodeOperator(opts.namespace);
- await nodeOper.init(client, registry);
+ nodeOper = new NodeOperator(
+ opts.namespace,
+ kubeConfig,
+ registry,
+ opts.watcherIdleTimeout
+ );
+ await nodeOper.init(kubeConfig);
await nodeOper.start();
- poolOper = new PoolOperator(opts.namespace);
- await poolOper.init(client, registry);
+ poolOper = new PoolOperator(
+ opts.namespace,
+ kubeConfig,
+ registry,
+ opts.watcherIdleTimeout
+ );
+ await poolOper.init(kubeConfig);
await poolOper.start();
}
- volumes = new Volumes(registry);
+ const volumes = new Volumes(registry);
volumes.start();
if (!opts.s) {
- volumeOper = new VolumeOperator(opts.namespace);
- await volumeOper.init(client, volumes);
+ volumeOper = new VolumeOperator(
+ opts.namespace,
+ kubeConfig,
+ volumes,
+ opts.watcherIdleTimeout
+ );
+ await volumeOper.init(kubeConfig);
await volumeOper.start();
}
- apiServer = new ApiServer(registry);
+ const apiServer = new ApiServer(registry);
await apiServer.start(opts.port);
csiServer.makeReady(registry, volumes);
diff --git a/csi/moac/mbus.js b/csi/moac/mbus.js
index 13daf71db..ac700fea0 100755
--- a/csi/moac/mbus.js
+++ b/csi/moac/mbus.js
@@ -51,17 +51,26 @@ const opts = yargs
const nc = nats.connect(opts.s);
nc.on('connect', () => {
if (opts._[0] === 'register') {
- nc.publish('register', JSON.stringify({
+ nc.publish('v0/registry', JSON.stringify({
+ id: "v0/register",
+ sender: "moac",
+ data: {
id: opts.node,
grpcEndpoint: opts.grpc
+ }
}));
} else if (opts._[0] === 'deregister') {
- nc.publish('deregister', JSON.stringify({
+ nc.publish('v0/registry', JSON.stringify({
+ id: "v0/deregister",
+ sender: "moac",
+ data: {
id: opts.node
+ }
}));
} else if (opts._[0] === 'raw') {
nc.publish(opts.name, opts.payload);
}
+ nc.flush();
nc.close();
process.exit(0);
});
diff --git a/csi/moac/nats.js b/csi/moac/nats.js
index cfcaa5644..cba658c4a 100644
--- a/csi/moac/nats.js
+++ b/csi/moac/nats.js
@@ -49,26 +49,28 @@ class MessageBus {
return this.connected;
}
+ // The method is async but returns immediately.
+ // However it's up to caller if she wants to wait for it.
_connect () {
log.debug(`Connecting to NATS at "${this.endpoint}" ...`);
if (this.timeout) clearTimeout(this.timeout);
assert(!this.nc);
- this.nc = nats.connect({
+ nats.connect({
servers: [`nats://${this.endpoint}`]
- });
- var self = this;
- this.nc.on('connect', () => {
- log.info(`Connected to NATS message bus at "${this.endpoint}"`);
- self.connected = true;
- self._subscribe();
- });
- this.nc.on('error', (err) => {
- log.error(`${err}`);
- self._disconnect();
- log.debug(`Reconnecting after ${self.reconnectDelay}ms`);
- // reconnect but give it some time to recover to prevent spinning in loop
- self.timeout = setTimeout(self._connect.bind(self), self.reconnectDelay);
- });
+ })
+ .then((nc) => {
+ log.info(`Connected to NATS message bus at "${this.endpoint}"`);
+ this.nc = nc;
+ this.connected = true;
+ this._subscribe();
+ })
+ .catch((err) => {
+ log.error(`${err}`);
+ this._disconnect();
+ log.debug(`Reconnecting after ${this.reconnectDelay}ms`);
+ // reconnect but give it some time to recover to prevent spinning in loop
+ this.timeout = setTimeout(this._connect.bind(this), this.reconnectDelay);
+ });
}
_disconnect () {
@@ -81,58 +83,59 @@ class MessageBus {
}
_parsePayload (msg) {
- if (typeof (msg.data) !== 'string') {
- log.error(`Invalid payload in ${msg.subject} message: not a string`);
- return;
- }
+ const sc = nats.StringCodec();
try {
- return JSON.parse(msg.data);
+ return JSON.parse(sc.decode(msg.data));
} catch (e) {
log.error(`Invalid payload in ${msg.subject} message: not a JSON`);
}
}
+ _registrationReceived (data) {
+ const ep = data.grpcEndpoint;
+ if (typeof ep !== 'string' || ep.length === 0) {
+ log.error('Invalid grpc endpoint in registration message');
+ return;
+ }
+ const id = data.id;
+ if (typeof id !== 'string' || id.length === 0) {
+ log.error('Invalid node name in registration message');
+ return;
+ }
+ log.trace(`"${id}" with "${ep}" requested registration`);
+ this.registry.addNode(id, ep);
+ }
+
+ _deregistrationReceived (data) {
+ const id = data.id;
+ if (typeof id !== 'string' || id.length === 0) {
+ log.error('Invalid node name in deregistration message');
+ return;
+ }
+ log.trace(`"${id}" requested deregistration`);
+ this.registry.removeNode(id);
+ }
+
_subscribe () {
- this.nc.subscribe('register', (err, msg) => {
- if (err) {
- log.error(`Error receiving a registration message: ${err}`);
- return;
- }
- const data = this._parsePayload(msg);
- if (!data) {
- return;
- }
- const ep = data.grpcEndpoint;
- if (typeof ep !== 'string' || ep.length === 0) {
- log.error('Invalid grpc endpoint in registration message');
- return;
- }
- const id = data.id;
- if (typeof id !== 'string' || id.length === 0) {
- log.error('Invalid node name in registration message');
- return;
- }
- log.trace(`"${id}" with "${ep}" requested registration`);
- this.registry.addNode(id, ep);
- });
+ const registrySub = this.nc.subscribe('v0/registry');
+ this._registryHandler(registrySub);
+ }
- this.nc.subscribe('deregister', (err, msg) => {
- if (err) {
- log.error(`Error receiving a deregistration message: ${err}`);
- return;
- }
- const data = this._parsePayload(msg);
- if (!data) {
+ async _registryHandler (sub) {
+ for await (const m of sub) {
+ const payload = this._parsePayload(m);
+ if (!payload) {
return;
}
- const id = data.id;
- if (typeof id !== 'string' || id.length === 0) {
- log.error('Invalid node name in deregistration message');
- return;
+ if (payload.id === 'v0/register') {
+ this._registrationReceived(payload.data);
+ } else if (payload.id === 'v0/deregister') {
+ this._deregistrationReceived(payload.data);
+ } else {
+ const id = payload.id;
+ log.error(`Unknown registry message: ${id}`);
}
- log.trace(`"${id}" requested deregistration`);
- this.registry.removeNode(id);
- });
+ }
}
}
diff --git a/csi/moac/node-composition.nix b/csi/moac/node-composition.nix
index 9988d2a1c..6441534a8 100644
--- a/csi/moac/node-composition.nix
+++ b/csi/moac/node-composition.nix
@@ -1,12 +1,9 @@
# This file has been generated by node2nix 1.8.0. Do not edit!
-{ pkgs ? import {
+{pkgs ? import {
inherit system;
- }
-, system ? builtins.currentSystem
-, nodejs-slim ? pkgs.nodejs-slim-12_x
-, nodejs ? pkgs."nodejs-12_x"
-}:
+ }, system ? builtins.currentSystem, nodejs-slim ? pkgs.nodejs-slim-12_x, nodejs ? pkgs."nodejs-12_x"}:
+
let
nodeEnv = import ./node-env.nix {
inherit (pkgs) stdenv python2 utillinux runCommand writeTextFile;
diff --git a/csi/moac/node-env.nix b/csi/moac/node-env.nix
index cd2c6b06f..4d35a5efa 100644
--- a/csi/moac/node-env.nix
+++ b/csi/moac/node-env.nix
@@ -1,12 +1,12 @@
# This file originates from node2nix
-{ stdenv, nodejs-slim, nodejs, python2, utillinux, libtool, runCommand, writeTextFile }:
+{stdenv, nodejs-slim, nodejs, python2, utillinux, libtool, runCommand, writeTextFile}:
+
let
python = if nodejs ? python then nodejs.python else python2;
# Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
- tarWrapper = runCommand "tarWrapper"
- { } ''
+ tarWrapper = runCommand "tarWrapper" {} ''
mkdir -p $out/bin
cat > $out/bin/tar < 0;
+ const wasOffline = this.syncFailed > 0;
if (wasOffline) {
this.syncFailed = 0;
}
@@ -210,21 +209,20 @@ class Node extends EventEmitter {
// @param {object[]} replicas New replicas with properties.
//
_mergePoolsAndReplicas (pools, replicas) {
- var self = this;
// detect modified and new pools
pools.forEach((props) => {
const poolReplicas = replicas.filter((r) => r.pool === props.name);
- const pool = self.pools.find((p) => p.name === props.name);
+ const pool = this.pools.find((p) => p.name === props.name);
if (pool) {
// the pool already exists - update it
pool.merge(props, poolReplicas);
} else {
// it is a new pool
- self._registerPool(new Pool(props), poolReplicas);
+ this._registerPool(new Pool(props), poolReplicas);
}
});
// remove pools that no longer exist
- self.pools
+ this.pools
.filter((p) => !pools.find((ent) => ent.name === p.name))
.forEach((p) => p.unbind());
}
@@ -242,20 +240,19 @@ class Node extends EventEmitter {
// @param {object[]} nexusList List of nexus obtained from storage node.
//
_mergeNexus (nexusList) {
- var self = this;
// detect modified and new pools
nexusList.forEach((props) => {
- const nexus = self.nexus.find((n) => n.uuid === props.uuid);
+ const nexus = this.nexus.find((n) => n.uuid === props.uuid);
if (nexus) {
// the nexus already exists - update it
nexus.merge(props);
} else {
// it is a new nexus
- self._registerNexus(new Nexus(props, []));
+ this._registerNexus(new Nexus(props, []));
}
});
// remove nexus that no longer exist
- const removedNexus = self.nexus.filter(
+ const removedNexus = this.nexus.filter(
(n) => !nexusList.find((ent) => ent.uuid === n.uuid)
);
removedNexus.forEach((n) => n.destroy());
@@ -339,7 +336,7 @@ class Node extends EventEmitter {
async createPool (name, disks) {
log.debug(`Creating pool "${name}@${this.name}" ...`);
- var poolInfo = await this.call('createPool', { name, disks });
+ const poolInfo = await this.call('createPool', { name, disks });
log.info(`Created pool "${name}@${this.name}"`);
const newPool = new Pool(poolInfo);
@@ -357,7 +354,7 @@ class Node extends EventEmitter {
const children = replicas.map((r) => r.uri);
log.debug(`Creating nexus "${uuid}@${this.name}"`);
- var nexusInfo = await this.call('createNexus', { uuid, size, children });
+ const nexusInfo = await this.call('createNexus', { uuid, size, children });
log.info(`Created nexus "${uuid}@${this.name}"`);
const newNexus = new Nexus(nexusInfo);
diff --git a/csi/moac/node_operator.js b/csi/moac/node_operator.js
deleted file mode 100644
index dee49e6e3..000000000
--- a/csi/moac/node_operator.js
+++ /dev/null
@@ -1,265 +0,0 @@
-// Node operator is responsible for managing mayastor node custom resources
-// that represent nodes in the cluster that run mayastor (storage nodes).
-//
-// Roles:
-// * The operator creates/modifies/deletes the resources to keep them up to date.
-// * A user can delete a stale resource (can happen that moac doesn't know)
-
-'use strict';
-
-const assert = require('assert');
-const fs = require('fs');
-const path = require('path');
-const yaml = require('js-yaml');
-const EventStream = require('./event_stream');
-const log = require('./logger').Logger('node-operator');
-const Watcher = require('./watcher');
-const Workq = require('./workq');
-
-const crdNode = yaml.safeLoad(
- fs.readFileSync(path.join(__dirname, '/crds/mayastornode.yaml'), 'utf8')
-);
-
-// Node operator watches k8s CSINode resources and based on that detects
-// running mayastor instances in the cluster.
-class NodeOperator {
- // init() is decoupled from constructor because tests do their own
- // initialization of the object.
- //
- // @param {string} namespace Namespace the operator should operate on.
- constructor (namespace) {
- this.k8sClient = null; // k8s client for sending requests to api srv
- this.watcher = null; // k8s resource watcher for CSI nodes resource
- this.registry = null;
- this.namespace = namespace;
- this.workq = new Workq(); // for serializing node operations
- }
-
- // Create node CRD if it doesn't exist and augment client object so that CRD
- // can be manipulated as any other standard k8s api object.
- //
- // @param {object} k8sClient Client for k8s api server.
- // @param {object} registry Registry with node objects.
- //
- async init (k8sClient, registry) {
- log.info('Initializing node operator');
- assert(registry);
-
- try {
- await k8sClient.apis[
- 'apiextensions.k8s.io'
- ].v1beta1.customresourcedefinitions.post({ body: crdNode });
- log.info('Created CRD ' + crdNode.spec.names.kind);
- } catch (err) {
- // API returns a 409 Conflict if CRD already exists.
- if (err.statusCode !== 409) throw err;
- }
- k8sClient.addCustomResourceDefinition(crdNode);
-
- this.k8sClient = k8sClient;
- this.registry = registry;
-
- // Initialize watcher with all callbacks for new/mod/del events
- this.watcher = new Watcher(
- 'node',
- this.k8sClient.apis['openebs.io'].v1alpha1.namespaces(
- this.namespace
- ).mayastornodes,
- this.k8sClient.apis['openebs.io'].v1alpha1.watch.namespaces(
- this.namespace
- ).mayastornodes,
- this._filterMayastorNode
- );
- }
-
- // Normalize k8s mayastor node resource.
- //
- // @param {object} msn MayaStor node custom resource.
- // @returns {object} Properties defining the node.
- //
- _filterMayastorNode (msn) {
- if (!msn.spec.grpcEndpoint) {
- log.warn('Ignoring mayastor node resource without grpc endpoint');
- return null;
- }
- return {
- metadata: { name: msn.metadata.name },
- spec: {
- grpcEndpoint: msn.spec.grpcEndpoint
- },
- status: msn.status || 'unknown'
- };
- }
-
- // Bind watcher's new/del events to node operator's callbacks.
- //
- // Not interested in mod events as the operator is the only who should
- // be doing modifications to these objects.
- //
- // @param {object} watcher k8s node resource watcher.
- //
- _bindWatcher (watcher) {
- var self = this;
- watcher.on('new', (obj) => {
- self.registry.addNode(obj.metadata.name, obj.spec.grpcEndpoint);
- });
- watcher.on('del', (obj) => {
- self.registry.removeNode(obj.metadata.name);
- });
- }
-
- // Start node operator's watcher loop.
- async start () {
- var self = this;
-
- // install event handlers to follow changes to resources.
- self._bindWatcher(self.watcher);
- await self.watcher.start();
-
- // This will start async processing of node events.
- self.eventStream = new EventStream({ registry: self.registry });
- self.eventStream.on('data', async (ev) => {
- if (ev.kind !== 'node') return;
-
- await self.workq.push(ev, self._onNodeEvent.bind(self));
- });
- }
-
- async _onNodeEvent (ev) {
- var self = this;
- const name = ev.object.name;
- if (ev.eventType === 'new' || ev.eventType === 'mod') {
- const endpoint = ev.object.endpoint;
- const k8sNode = await self.watcher.getRawBypass(name);
-
- if (k8sNode) {
- // Update object only if it has really changed
- if (k8sNode.spec.grpcEndpoint !== endpoint) {
- try {
- await self._updateResource(name, k8sNode, endpoint);
- } catch (err) {
- log.error(`Failed to update node resource "${name}": ${err}`);
- return;
- }
- }
- } else if (ev.eventType === 'new') {
- try {
- await self._createResource(name, endpoint);
- } catch (err) {
- log.error(`Failed to create node resource "${name}": ${err}`);
- return;
- }
- }
-
- await this._updateStatus(name, ev.object.isSynced() ? 'online' : 'offline');
- } else if (ev.eventType === 'del') {
- await self._deleteResource(ev.object.name);
- } else {
- assert.strictEqual(ev.eventType, 'sync');
- }
- }
-
- // Create k8s CRD object.
- //
- // @param {string} name Node of the created node.
- // @param {string} grpcEndpoint Endpoint property of the object.
- //
- async _createResource (name, grpcEndpoint) {
- log.info(`Creating node resource "${name}"`);
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastornodes.post({
- body: {
- apiVersion: 'openebs.io/v1alpha1',
- kind: 'MayastorNode',
- metadata: {
- name,
- namespace: this.namespace
- },
- spec: { grpcEndpoint }
- }
- });
- }
-
- // Update properties of k8s CRD object or create it if it does not exist.
- //
- // @param {string} name Name of the updated node.
- // @param {object} k8sNode Existing k8s resource object.
- // @param {string} grpcEndpoint Endpoint property of the object.
- //
- async _updateResource (name, k8sNode, grpcEndpoint) {
- log.info(`Updating spec of node resource "${name}"`);
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastornodes(name)
- .put({
- body: {
- apiVersion: 'openebs.io/v1alpha1',
- kind: 'MayastorNode',
- metadata: k8sNode.metadata,
- spec: { grpcEndpoint }
- }
- });
- }
-
- // Update state of the resource.
- //
- // NOTE: This method does not throw if the operation fails as there is nothing
- // we can do if it fails. Though we log an error message in such a case.
- //
- // @param {string} name UUID of the resource.
- // @param {string} status State of the node.
- //
- async _updateStatus (name, status) {
- var k8sNode = await this.watcher.getRawBypass(name);
- if (!k8sNode) {
- log.warn(
- `Wanted to update state of node resource "${name}" that disappeared`
- );
- return;
- }
- if (k8sNode.status === status) {
- // avoid unnecessary status updates
- return;
- }
- log.debug(`Updating status of node resource "${name}"`);
- k8sNode.status = status;
- try {
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastornodes(name)
- .status.put({ body: k8sNode });
- } catch (err) {
- log.error(`Failed to update status of node resource "${name}": ${err}`);
- }
- }
-
- // Delete node resource with specified name.
- //
- // @param {string} name Name of the node resource to delete.
- //
- async _deleteResource (name) {
- var k8sNode = await this.watcher.getRawBypass(name);
- if (k8sNode) {
- log.info(`Deleting node resource "${name}"`);
- try {
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastornodes(name)
- .delete();
- } catch (err) {
- log.error(`Failed to delete node resource "${name}": ${err}`);
- }
- }
- }
-
- // Stop listening for watcher and node events and reset the cache
- async stop () {
- this.watcher.removeAllListeners();
- await this.watcher.stop();
- this.eventStream.destroy();
- this.eventStream = null;
- }
-}
-
-module.exports = NodeOperator;
diff --git a/csi/moac/node_operator.ts b/csi/moac/node_operator.ts
new file mode 100644
index 000000000..ce6b87c18
--- /dev/null
+++ b/csi/moac/node_operator.ts
@@ -0,0 +1,278 @@
+// Node operator is responsible for managing mayastor node custom resources
+// that represent nodes in the cluster that run mayastor (storage nodes).
+//
+// Roles:
+// * The operator creates/modifies/deletes the resources to keep them up to date.
+// * A user can delete a stale resource (can happen that moac doesn't know)
+
+import assert from 'assert';
+import * as fs from 'fs';
+import * as path from 'path';
+import {
+ ApiextensionsV1Api,
+ KubeConfig,
+} from 'client-node-fixed-watcher';
+import {
+ CustomResource,
+ CustomResourceCache,
+ CustomResourceMeta,
+} from './watcher';
+
+const yaml = require('js-yaml');
+const EventStream = require('./event_stream');
+const log = require('./logger').Logger('node-operator');
+const Workq = require('./workq');
+
+const RESOURCE_NAME: string = 'mayastornode';
+const crdNode = yaml.safeLoad(
+ fs.readFileSync(path.join(__dirname, '/crds/mayastornode.yaml'), 'utf8')
+);
+
+// State of a storage node.
+enum NodeState {
+ Unknown = "unknown",
+ Online = "online",
+ Offline = "offline",
+}
+
+// Object defines properties of node resource.
+export class NodeResource extends CustomResource {
+ apiVersion?: string;
+ kind?: string;
+ metadata: CustomResourceMeta;
+ spec: { grpcEndpoint: string };
+ status?: NodeState;
+
+ constructor(cr: CustomResource) {
+ super();
+ this.apiVersion = cr.apiVersion;
+ this.kind = cr.kind;
+ if (cr.status === NodeState.Online) {
+ this.status = NodeState.Online;
+ } else if (cr.status === NodeState.Offline) {
+ this.status = NodeState.Offline;
+ } else {
+ this.status = NodeState.Unknown;
+ }
+ if (cr.metadata === undefined) {
+ throw new Error('missing metadata');
+ } else {
+ this.metadata = cr.metadata;
+ }
+ if (cr.spec === undefined) {
+ throw new Error('missing spec');
+ } else {
+ let grpcEndpoint = (cr.spec as any).grpcEndpoint;
+ if (grpcEndpoint === undefined) {
+ throw new Error('missing grpc endpoint in spec');
+ }
+ this.spec = { grpcEndpoint };
+ }
+ }
+}
+
+export class NodeOperator {
+ watcher: CustomResourceCache; // k8s resource watcher for nodes
+ registry: any;
+ namespace: string;
+ workq: any; // for serializing node operations
+ eventStream: any; // events from the registry
+
+ // Create node operator object.
+ //
+ // @param namespace Namespace the operator should operate on.
+ // @param kubeConfig KubeConfig.
+ // @param registry Registry with node objects.
+ // @param [idleTimeout] Timeout for restarting watcher connection when idle.
+ constructor (
+ namespace: string,
+ kubeConfig: KubeConfig,
+ registry: any,
+ idleTimeout: number | undefined,
+ ) {
+ assert(registry);
+ this.namespace = namespace;
+ this.workq = new Workq();
+ this.registry = registry;
+ this.watcher = new CustomResourceCache(
+ this.namespace,
+ RESOURCE_NAME,
+ kubeConfig,
+ NodeResource,
+ { idleTimeout }
+ );
+ }
+
+ // Create node CRD if it doesn't exist.
+ //
+ // @param kubeConfig KubeConfig.
+ async init (kubeConfig: KubeConfig) {
+ log.info('Initializing node operator');
+ let k8sExtApi = kubeConfig.makeApiClient(ApiextensionsV1Api);
+ try {
+ await k8sExtApi.createCustomResourceDefinition(crdNode);
+ log.info(`Created CRD ${RESOURCE_NAME}`);
+ } catch (err) {
+ // API returns a 409 Conflict if CRD already exists.
+ if (err.statusCode !== 409) throw err;
+ }
+ }
+
+ // Bind watcher's new/del events to node operator's callbacks.
+ //
+ // Not interested in mod events as the operator is the only who should
+ // be doing modifications to these objects.
+ //
+ // @param {object} watcher k8s node resource watcher.
+ //
+ _bindWatcher (watcher: CustomResourceCache) {
+ watcher.on('new', (obj: NodeResource) => {
+ if (obj.metadata) {
+ this.registry.addNode(obj.metadata.name, obj.spec.grpcEndpoint);
+ }
+ });
+ watcher.on('del', (obj: NodeResource) => {
+ this.registry.removeNode(obj.metadata.name);
+ });
+ }
+
+ // Start node operator's watcher loop.
+ async start () {
+ // install event handlers to follow changes to resources.
+ this._bindWatcher(this.watcher);
+ await this.watcher.start();
+
+ // This will start async processing of node events.
+ this.eventStream = new EventStream({ registry: this.registry });
+ this.eventStream.on('data', async (ev: any) => {
+ if (ev.kind !== 'node') return;
+ await this.workq.push(ev, this._onNodeEvent.bind(this));
+ });
+ }
+
+ async _onNodeEvent (ev: any) {
+ const name = ev.object.name;
+ if (ev.eventType === 'new') {
+ const grpcEndpoint = ev.object.endpoint;
+ let origObj = this.watcher.get(name);
+ if (origObj === undefined) {
+ await this._createResource(name, grpcEndpoint);
+ } else {
+ await this._updateSpec(name, grpcEndpoint);
+ }
+ await this._updateStatus(
+ name,
+ ev.object.isSynced() ? NodeState.Online : NodeState.Offline,
+ );
+ } else if (ev.eventType === 'mod') {
+ const grpcEndpoint = ev.object.endpoint;
+ let origObj = this.watcher.get(name);
+ // The node might be just going away - do nothing if not in the cache
+ if (origObj !== undefined) {
+ await this._updateSpec(name, grpcEndpoint);
+ await this._updateStatus(
+ name,
+ ev.object.isSynced() ? NodeState.Online : NodeState.Offline,
+ );
+ }
+ } else if (ev.eventType === 'del') {
+ await this._deleteResource(ev.object.name);
+ } else {
+ assert.strictEqual(ev.eventType, 'sync');
+ }
+ }
+
+ async _createResource(name: string, grpcEndpoint: string) {
+ log.info(`Creating node resource "${name}"`);
+ try {
+ await this.watcher.create({
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorNode',
+ metadata: {
+ name,
+ namespace: this.namespace
+ },
+ spec: { grpcEndpoint }
+ });
+ } catch (err) {
+ log.error(`Failed to create node resource "${name}": ${err}`);
+ }
+ }
+
+ // Update properties of k8s CRD object or create it if it does not exist.
+ //
+ // @param name Name of the updated node.
+ // @param grpcEndpoint Endpoint property of the object.
+ //
+ async _updateSpec (name: string, grpcEndpoint: string) {
+ try {
+ await this.watcher.update(name, (orig: NodeResource) => {
+ // Update object only if it has really changed
+ if (orig.spec.grpcEndpoint === grpcEndpoint) {
+ return;
+ }
+ log.info(`Updating spec of node resource "${name}"`);
+ return {
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorNode',
+ metadata: orig.metadata,
+ spec: { grpcEndpoint }
+ };
+ });
+ } catch (err) {
+ log.error(`Failed to update node resource "${name}": ${err}`);
+ }
+ }
+
+ // Update state of the resource.
+ //
+ // NOTE: This method does not throw if the operation fails as there is nothing
+ // we can do if it fails. Though we log an error message in such a case.
+ //
+ // @param name UUID of the resource.
+ // @param status State of the node.
+ //
+ async _updateStatus (name: string, status: NodeState) {
+ try {
+ await this.watcher.updateStatus(name, (orig: NodeResource) => {
+ // avoid unnecessary status updates
+ if (orig.status === status) {
+ return;
+ }
+ log.debug(`Updating status of node resource "${name}"`);
+ return {
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorNode',
+ metadata: orig.metadata,
+ spec: orig.spec,
+ status: status,
+ };
+ });
+ } catch (err) {
+ log.error(`Failed to update status of node resource "${name}": ${err}`);
+ }
+ }
+
+ // Delete node resource with specified name.
+ //
+ // @param {string} name Name of the node resource to delete.
+ //
+ async _deleteResource (name: string) {
+ try {
+ log.info(`Deleting node resource "${name}"`);
+ await this.watcher.delete(name);
+ } catch (err) {
+ log.error(`Failed to delete node resource "${name}": ${err}`);
+ }
+ }
+
+ // Stop listening for watcher and node events and reset the cache
+ stop () {
+ this.watcher.stop();
+ this.watcher.removeAllListeners();
+ if (this.eventStream) {
+ this.eventStream.destroy();
+ this.eventStream = null;
+ }
+ }
+}
diff --git a/csi/moac/package-lock.json b/csi/moac/package-lock.json
index 2cb6e658a..f6e7caf33 100644
--- a/csi/moac/package-lock.json
+++ b/csi/moac/package-lock.json
@@ -5,76 +5,134 @@
"requires": true,
"dependencies": {
"@babel/code-frame": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
- "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==",
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
+ "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
"dev": true,
"requires": {
- "@babel/highlight": "^7.10.1"
+ "@babel/highlight": "^7.10.4"
}
},
"@babel/helper-validator-identifier": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz",
- "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==",
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz",
+ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==",
"dev": true
},
"@babel/highlight": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz",
- "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==",
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz",
+ "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==",
"dev": true,
"requires": {
- "@babel/helper-validator-identifier": "^7.10.1",
+ "@babel/helper-validator-identifier": "^7.10.4",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
}
},
- "@grpc/proto-loader": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.4.tgz",
- "integrity": "sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==",
+ "@dabh/diagnostics": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
+ "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==",
"requires": {
- "lodash.camelcase": "^4.3.0",
- "protobufjs": "^6.8.6"
+ "colorspace": "1.1.x",
+ "enabled": "2.0.x",
+ "kuler": "^2.0.0"
}
},
- "@kubernetes/client-node": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.10.2.tgz",
- "integrity": "sha512-JvsmxbTwiMqsh9LyuXMzT5HjoENFbB3a/JroJsobuAzkxN162UqAOvg++/AA+ccIMWRR2Qln4FyaOJ0a4eKyXg==",
+ "@eslint/eslintrc": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.1.tgz",
+ "integrity": "sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA==",
+ "dev": true,
"requires": {
- "@types/js-yaml": "^3.12.1",
- "@types/node": "^10.12.0",
- "@types/request": "^2.47.1",
- "@types/underscore": "^1.8.9",
- "@types/ws": "^6.0.1",
- "isomorphic-ws": "^4.0.1",
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^12.1.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
"js-yaml": "^3.13.1",
- "json-stream": "^1.0.0",
- "jsonpath-plus": "^0.19.0",
- "request": "^2.88.0",
- "shelljs": "^0.8.2",
- "tslib": "^1.9.3",
- "underscore": "^1.9.1",
- "ws": "^6.1.0"
+ "lodash": "^4.17.19",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
},
"dependencies": {
- "@types/node": {
- "version": "10.17.24",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.24.tgz",
- "integrity": "sha512-5SCfvCxV74kzR3uWgTYiGxrd69TbT1I6+cMx1A5kEly/IVveJBimtAMlXiEyVFn5DvUFewQWxOOiJhlxeQwxgA=="
- },
- "ws": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
- "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
+ "debug": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+ "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
+ "dev": true,
"requires": {
- "async-limiter": "~1.0.0"
+ "ms": "2.1.2"
}
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true
}
}
},
+ "@grpc/proto-loader": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.5.tgz",
+ "integrity": "sha512-WwN9jVNdHRQoOBo9FDH7qU+mgfjPc8GygPYms3M+y3fbQLfnCe/Kv/E01t7JRgnrsOHH8euvSbed3mIalXhwqQ==",
+ "requires": {
+ "lodash.camelcase": "^4.3.0",
+ "protobufjs": "^6.8.6"
+ }
+ },
"@panva/asn1.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
@@ -135,23 +193,23 @@
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"@sindresorhus/is": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
- "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.0.tgz",
+ "integrity": "sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ=="
},
"@sinonjs/commons": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz",
- "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz",
+ "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/fake-timers": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.0.tgz",
- "integrity": "sha512-atR1J/jRXvQAb47gfzSK8zavXy7BcpnYq21ALon0U99etu99vsir0trzIO3wpeLtW+LLVY6X7EkfVTbjGSH8Ww==",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz",
+ "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.7.0"
@@ -168,9 +226,9 @@
}
},
"@sinonjs/samsam": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz",
- "integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.1.0.tgz",
+ "integrity": "sha512-42nyaQOVunX5Pm6GRJobmzbS7iLI+fhERITnETXzzwDZh+TtDr/Au3yAvXVjFmZ4wEUaE4Y3NFZfKv0bV0cbtg==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.6.0",
@@ -185,11 +243,22 @@
"dev": true
},
"@szmarczak/http-timer": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
- "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz",
+ "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==",
+ "requires": {
+ "defer-to-connect": "^2.0.0"
+ }
+ },
+ "@types/cacheable-request": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz",
+ "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==",
"requires": {
- "defer-to-connect": "^1.0.1"
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "*",
+ "@types/node": "*",
+ "@types/responselike": "*"
}
},
"@types/caseless": {
@@ -197,35 +266,52 @@
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
},
- "@types/color-name": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
- "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
+ "@types/http-cache-semantics": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz",
+ "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A=="
+ },
+ "@types/js-yaml": {
+ "version": "3.12.5",
+ "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz",
+ "integrity": "sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww=="
+ },
+ "@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
+ "dev": true
},
- "@types/got": {
- "version": "9.6.11",
- "resolved": "https://registry.npmjs.org/@types/got/-/got-9.6.11.tgz",
- "integrity": "sha512-dr3IiDNg5TDesGyuwTrN77E1Cd7DCdmCFtEfSGqr83jMMtcwhf/SGPbN2goY4JUWQfvxwY56+e5tjfi+oXeSdA==",
+ "@types/keyv": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz",
+ "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==",
"requires": {
- "@types/node": "*",
- "@types/tough-cookie": "*",
- "form-data": "^2.5.0"
+ "@types/node": "*"
}
},
- "@types/js-yaml": {
- "version": "3.12.4",
- "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.4.tgz",
- "integrity": "sha512-fYMgzN+9e28R81weVN49inn/u798ruU91En1ZnGvSZzCRc5jXx9B2EDhlRaWmcO1RIxFHL8AajRXzxDuJu93+A=="
+ "@types/lodash": {
+ "version": "4.14.161",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.161.tgz",
+ "integrity": "sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA=="
},
"@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
},
+ "@types/minipass": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/minipass/-/minipass-2.2.0.tgz",
+ "integrity": "sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/node": {
- "version": "13.13.9",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.9.tgz",
- "integrity": "sha512-EPZBIGed5gNnfWCiwEIwTE2Jdg4813odnG8iNPMQGrqVxrI+wL68SPtPeCX+ZxGBaA6pKAVc6jaKgP/Q0QzfdQ=="
+ "version": "13.13.23",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.23.tgz",
+ "integrity": "sha512-L31WmMJYKb15PDqFWutn8HNwrNK6CE6bkWgSB0dO1XpNoHrszVKV1Clcnfgd6c/oG54TVF8XQEvY2gQrW8K6Mw=="
},
"@types/request": {
"version": "2.48.5",
@@ -238,15 +324,40 @@
"form-data": "^2.5.0"
}
},
+ "@types/responselike": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
+ "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/stream-buffers": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.3.tgz",
+ "integrity": "sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ==",
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/tar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/tar/-/tar-4.0.3.tgz",
+ "integrity": "sha512-Z7AVMMlkI8NTWF0qGhC4QIX0zkV/+y0J8x7b/RsHrN0310+YNjoJd8UrApCiGBCWtKjxS9QhNqLi2UJNToh5hA==",
+ "requires": {
+ "@types/minipass": "*",
+ "@types/node": "*"
+ }
+ },
"@types/tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A=="
},
"@types/underscore": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.0.tgz",
- "integrity": "sha512-ZAbqul7QAKpM2h1PFGa5ETN27ulmqtj0QviYHasw9LffvXZvVHuraOx/FOsIPPDNGZN0Qo1nASxxSfMYOtSoCw=="
+ "version": "1.10.24",
+ "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.24.tgz",
+ "integrity": "sha512-T3NQD8hXNW2sRsSbLNjF/aBo18MyJlbw0lSpQHB/eZZtScPdexN4HSa8cByYwTw9Wy7KuOFr81mlDQcQQaZ79w=="
},
"@types/ws": {
"version": "6.0.4",
@@ -266,30 +377,30 @@
}
},
"acorn": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz",
- "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==",
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true
},
"acorn-jsx": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
- "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
+ "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
"dev": true
},
"aggregate-error": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz",
- "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"requires": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
}
},
"ajv": {
- "version": "6.12.2",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
- "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -298,15 +409,9 @@
}
},
"ansi-colors": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
- "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
- "dev": true
- },
- "ansi-escapes": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
- "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
"dev": true
},
"ansi-regex": {
@@ -315,12 +420,29 @@
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
- "color-convert": "^1.9.0"
+ "color-convert": "^2.0.1"
+ },
+ "dependencies": {
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ }
}
},
"anymatch": {
@@ -357,6 +479,39 @@
"is-string": "^1.0.5"
}
},
+ "array.prototype.flat": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz",
+ "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1"
+ }
+ },
+ "array.prototype.flatmap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz",
+ "integrity": "sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "function-bind": "^1.1.1"
+ }
+ },
+ "array.prototype.map": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz",
+ "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "es-array-method-boxes-properly": "^1.0.0",
+ "is-string": "^1.0.4"
+ }
+ },
"ascli": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz",
@@ -392,17 +547,9 @@
"dev": true
},
"async": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
- "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
- "requires": {
- "lodash": "^4.17.14"
- }
- },
- "async-limiter": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
- "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
+ "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
},
"asynckit": {
"version": "0.4.0",
@@ -415,9 +562,9 @@
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
"aws4": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz",
- "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA=="
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
+ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"balanced-match": {
"version": "1.0.0",
@@ -438,9 +585,9 @@
}
},
"binary-extensions": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
- "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+ "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
"dev": true
},
"body-parser": {
@@ -458,6 +605,13 @@
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
+ },
+ "dependencies": {
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ }
}
},
"brace-expansion": {
@@ -484,6 +638,17 @@
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
"dev": true
},
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
+ "dev": true
+ },
+ "byline": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz",
+ "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE="
+ },
"bytebuffer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz",
@@ -504,35 +669,45 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
+ "cacheable-lookup": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz",
+ "integrity": "sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w=="
+ },
"cacheable-request": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
- "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz",
+ "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==",
"requires": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
- "keyv": "^3.0.0",
+ "keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
"normalize-url": "^4.1.0",
- "responselike": "^1.0.2"
+ "responselike": "^2.0.0"
},
"dependencies": {
"get-stream": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz",
- "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"requires": {
"pump": "^3.0.0"
}
- },
- "lowercase-keys": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
- "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
}
}
},
+ "call-bind": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz",
+ "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.0"
+ }
+ },
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -564,33 +739,15 @@
}
},
"chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
+ "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- },
- "dependencies": {
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "requires": {
- "has-flag": "^3.0.0"
- }
- }
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
}
},
- "chardet": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
- "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
- "dev": true
- },
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
@@ -598,41 +755,67 @@
"dev": true
},
"chokidar": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
- "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
+ "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
- "fsevents": "~2.1.1",
+ "fsevents": "~2.1.2",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
- "readdirp": "~3.2.0"
+ "readdirp": "~3.4.0"
}
},
+ "chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
+ },
"clean-stack": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
},
- "cli-cursor": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
- "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
- "dev": true,
+ "client-node-fixed-watcher": {
+ "version": "0.13.2",
+ "resolved": "https://registry.npmjs.org/client-node-fixed-watcher/-/client-node-fixed-watcher-0.13.2.tgz",
+ "integrity": "sha512-Ze0lahaDt28q9OnYZDTMOKq2zJs64ETwyfWEOMjUErtY7hXjL7z725Nu5Ghfb3Fagujy/bSJ2QUXRuNioQqC8w==",
"requires": {
- "restore-cursor": "^2.0.0"
+ "@types/js-yaml": "^3.12.1",
+ "@types/node": "^10.12.0",
+ "@types/request": "^2.47.1",
+ "@types/stream-buffers": "^3.0.3",
+ "@types/tar": "^4.0.3",
+ "@types/underscore": "^1.8.9",
+ "@types/ws": "^6.0.1",
+ "byline": "^5.0.0",
+ "execa": "1.0.0",
+ "isomorphic-ws": "^4.0.1",
+ "js-yaml": "^3.13.1",
+ "jsonpath-plus": "^0.19.0",
+ "openid-client": "^4.1.1",
+ "request": "^2.88.0",
+ "rfc4648": "^1.3.0",
+ "shelljs": "^0.8.2",
+ "stream-buffers": "^3.0.2",
+ "tar": "^6.0.2",
+ "tmp-promise": "^3.0.2",
+ "tslib": "^1.9.3",
+ "underscore": "^1.9.1",
+ "ws": "^7.3.1"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "10.17.44",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.44.tgz",
+ "integrity": "sha512-vHPAyBX1ffLcy4fQHmDyIUMUb42gHZjPHU66nhvbMzAWJqHnySGZ6STwN3rwrnSd1FHB0DI/RWgGELgKSYRDmw=="
+ }
}
},
- "cli-width": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
- "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
- "dev": true
- },
"cliui": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
@@ -687,11 +870,6 @@
"simple-swizzle": "^0.2.2"
}
},
- "colornames": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz",
- "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y="
- },
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@@ -736,6 +914,13 @@
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"requires": {
"safe-buffer": "5.1.2"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ }
}
},
"content-type": {
@@ -762,7 +947,6 @@
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
- "dev": true,
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
@@ -787,23 +971,24 @@
"ms": "2.0.0"
}
},
- "debug-log": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz",
- "integrity": "sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8=",
- "dev": true
- },
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decompress-response": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
- "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"requires": {
- "mimic-response": "^1.0.0"
+ "mimic-response": "^3.1.0"
+ },
+ "dependencies": {
+ "mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
+ }
}
},
"deep-eql": {
@@ -821,47 +1006,19 @@
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"dev": true
},
- "deepmerge": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
- "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
- },
"defer-to-connect": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
- "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz",
+ "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg=="
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
- "dev": true,
"requires": {
"object-keys": "^1.0.12"
}
},
- "deglob": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/deglob/-/deglob-4.0.1.tgz",
- "integrity": "sha512-/g+RDZ7yf2HvoW+E5Cy+K94YhgcFgr6C8LuHZD1O5HoNPkf3KY6RfXJ0DBGlB/NkLi5gml+G9zqRzk9S0mHZCg==",
- "dev": true,
- "requires": {
- "find-root": "^1.0.0",
- "glob": "^7.0.5",
- "ignore": "^5.0.0",
- "pkg-config": "^1.1.0",
- "run-parallel": "^1.1.2",
- "uniq": "^1.0.1"
- },
- "dependencies": {
- "ignore": {
- "version": "5.1.8",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
- "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
- "dev": true
- }
- }
- },
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -877,20 +1034,10 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
- "diagnostics": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz",
- "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==",
- "requires": {
- "colorspace": "1.1.x",
- "enabled": "1.0.x",
- "kuler": "1.0.x"
- }
- },
"diff": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
- "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"dirty-chai": {
@@ -908,11 +1055,6 @@
"esutils": "^2.0.2"
}
},
- "duplexer3": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
- "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
- },
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
@@ -934,12 +1076,9 @@
"dev": true
},
"enabled": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz",
- "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=",
- "requires": {
- "env-variable": "0.0.x"
- }
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
+ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"encodeurl": {
"version": "1.0.2",
@@ -954,10 +1093,14 @@
"once": "^1.4.0"
}
},
- "env-variable": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz",
- "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg=="
+ "enquirer": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+ "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "^4.1.1"
+ }
},
"error-ex": {
"version": "1.3.2",
@@ -976,105 +1119,201 @@
}
}
},
- "es-abstract": {
- "version": "1.17.4",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz",
- "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==",
+ "es-abstract": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
+ "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ },
+ "object.assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
+ "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.18.0-next.0",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ }
+ }
+ }
+ }
+ },
+ "es-array-method-boxes-properly": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
+ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
+ "dev": true
+ },
+ "es-get-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz",
+ "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==",
"dev": true,
"requires": {
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
+ "es-abstract": "^1.17.4",
"has-symbols": "^1.0.1",
- "is-callable": "^1.1.5",
- "is-regex": "^1.0.5",
- "object-inspect": "^1.7.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.0",
- "string.prototype.trimleft": "^2.1.1",
- "string.prototype.trimright": "^2.1.1"
+ "is-arguments": "^1.0.4",
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-string": "^1.0.5",
+ "isarray": "^2.0.5"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ }
}
},
"es-to-primitive": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
"integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
- "dev": true,
"requires": {
"is-callable": "^1.1.4",
"is-date-object": "^1.0.1",
"is-symbol": "^1.0.2"
}
},
+ "escalade": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz",
+ "integrity": "sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig=="
+ },
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true
},
"eslint": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.4.0.tgz",
- "integrity": "sha512-WTVEzK3lSFoXUovDHEbkJqCVPEPwbhCq4trDktNI6ygs7aO41d4cDT0JFAT5MivzZeVLWlg7vHL+bgrQv/t3vA==",
+ "version": "7.12.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.12.1.tgz",
+ "integrity": "sha512-HlMTEdr/LicJfN08LB3nM1rRYliDXOmfoO4vj39xN6BLpFzF00hbwBoqHk8UcJ2M/3nlARZWy/mslvGEuZFvsg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
+ "@eslint/eslintrc": "^0.2.1",
"ajv": "^6.10.0",
- "chalk": "^2.1.0",
- "cross-spawn": "^6.0.5",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
"debug": "^4.0.1",
"doctrine": "^3.0.0",
- "eslint-scope": "^5.0.0",
- "eslint-utils": "^1.4.2",
- "eslint-visitor-keys": "^1.1.0",
- "espree": "^6.1.1",
- "esquery": "^1.0.1",
+ "enquirer": "^2.3.5",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.0",
+ "esquery": "^1.2.0",
"esutils": "^2.0.2",
"file-entry-cache": "^5.0.1",
"functional-red-black-tree": "^1.0.1",
"glob-parent": "^5.0.0",
- "globals": "^11.7.0",
+ "globals": "^12.1.0",
"ignore": "^4.0.6",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
- "inquirer": "^6.4.1",
"is-glob": "^4.0.0",
"js-yaml": "^3.13.1",
"json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.3.0",
- "lodash": "^4.17.14",
+ "levn": "^0.4.1",
+ "lodash": "^4.17.19",
"minimatch": "^3.0.4",
- "mkdirp": "^0.5.1",
"natural-compare": "^1.4.0",
- "optionator": "^0.8.2",
+ "optionator": "^0.9.1",
"progress": "^2.0.0",
- "regexpp": "^2.0.1",
- "semver": "^6.1.2",
- "strip-ansi": "^5.2.0",
- "strip-json-comments": "^3.0.1",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
"table": "^5.2.3",
"text-table": "^0.2.0",
"v8-compile-cache": "^2.0.3"
},
"dependencies": {
"ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+ "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
"debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
+ "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
- "ms": "^2.1.1"
+ "ms": "2.1.2"
}
},
"ms": {
@@ -1083,51 +1322,81 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
"semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "version": "7.3.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
+ "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
+ "dev": true
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true
},
"strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
- "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+ "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
- "ansi-regex": "^4.1.0"
+ "ansi-regex": "^5.0.0"
}
},
"strip-json-comments": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz",
- "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
}
}
},
"eslint-config-semistandard": {
- "version": "15.0.0",
- "resolved": "https://registry.npmjs.org/eslint-config-semistandard/-/eslint-config-semistandard-15.0.0.tgz",
- "integrity": "sha512-volIMnosUvzyxGkYUA5QvwkahZZLeUx7wcS0+7QumPn+MMEBbV6P7BY1yukamMst0w3Et3QZlCjQEwQ8tQ6nug==",
+ "version": "15.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-config-semistandard/-/eslint-config-semistandard-15.0.1.tgz",
+ "integrity": "sha512-sfV+qNBWKOmF0kZJll1VH5XqOAdTmLlhbOl9WKI11d2eMEe+Kicxnpm24PQWHOqAfk5pAWU2An0LjNCXKa4Usg==",
"dev": true
},
"eslint-config-standard": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-14.1.0.tgz",
- "integrity": "sha512-EF6XkrrGVbvv8hL/kYa/m6vnvmUT+K82pJJc4JJVMM6+Qgqh0pnwprSxdduDLB9p/7bIxD+YV5O0wfb8lmcPbA==",
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.0.tgz",
+ "integrity": "sha512-kMCehB9yXIG+LNsu9uXfm06o6Pt63TFAOzn9tUOzw4r/hFIxHhNR1Xomxy+B5zMrXhqyfHVEcmanzttEjGei9w==",
"dev": true
},
"eslint-config-standard-jsx": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-8.1.0.tgz",
- "integrity": "sha512-ULVC8qH8qCqbU792ZOO6DaiaZyHNS/5CZt3hKqHkEhVlhPEPN3nfBqqxJCyp59XrjIBZPu1chMYe9T2DXZ7TMw==",
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-10.0.0.tgz",
+ "integrity": "sha512-hLeA2f5e06W1xyr/93/QJulN/rLbUVUmqTlexv9PRKHFwEC9ffJcH2LvJhMoEqYQBEYafedgGZXH2W8NUpt5lA==",
"dev": true
},
"eslint-import-resolver-node": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz",
- "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==",
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz",
+ "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==",
"dev": true,
"requires": {
"debug": "^2.6.9",
@@ -1145,40 +1414,34 @@
}
},
"eslint-plugin-es": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-2.0.0.tgz",
- "integrity": "sha512-f6fceVtg27BR02EYnBhgWLFQfK6bN4Ll0nQFrBHOlCsAyxeZkn0NHns5O0YZOPrV1B3ramd6cgFwaoFLcSkwEQ==",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz",
+ "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==",
"dev": true,
"requires": {
- "eslint-utils": "^1.4.2",
+ "eslint-utils": "^2.0.0",
"regexpp": "^3.0.0"
- },
- "dependencies": {
- "regexpp": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
- "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
- "dev": true
- }
}
},
"eslint-plugin-import": {
- "version": "2.18.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz",
- "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==",
+ "version": "2.22.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz",
+ "integrity": "sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==",
"dev": true,
"requires": {
- "array-includes": "^3.0.3",
+ "array-includes": "^3.1.1",
+ "array.prototype.flat": "^1.2.3",
"contains-path": "^0.1.0",
"debug": "^2.6.9",
"doctrine": "1.5.0",
- "eslint-import-resolver-node": "^0.3.2",
- "eslint-module-utils": "^2.4.0",
+ "eslint-import-resolver-node": "^0.3.4",
+ "eslint-module-utils": "^2.6.0",
"has": "^1.0.3",
"minimatch": "^3.0.4",
- "object.values": "^1.1.0",
+ "object.values": "^1.1.1",
"read-pkg-up": "^2.0.0",
- "resolve": "^1.11.0"
+ "resolve": "^1.17.0",
+ "tsconfig-paths": "^3.9.0"
},
"dependencies": {
"doctrine": {
@@ -1194,13 +1457,13 @@
}
},
"eslint-plugin-node": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-10.0.0.tgz",
- "integrity": "sha512-1CSyM/QCjs6PXaT18+zuAXsjXGIGo5Rw630rSKwokSs2jrYURQc4R5JZpoanNCqwNmepg+0eZ9L7YiRUJb8jiQ==",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz",
+ "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==",
"dev": true,
"requires": {
- "eslint-plugin-es": "^2.0.0",
- "eslint-utils": "^1.4.2",
+ "eslint-plugin-es": "^3.0.0",
+ "eslint-utils": "^2.0.0",
"ignore": "^5.1.1",
"minimatch": "^3.0.4",
"resolve": "^1.10.1",
@@ -1228,20 +1491,22 @@
"dev": true
},
"eslint-plugin-react": {
- "version": "7.14.3",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz",
- "integrity": "sha512-EzdyyBWC4Uz2hPYBiEJrKCUi2Fn+BJ9B/pJQcjw5X+x/H2Nm59S4MJIvL4O5NEE0+WbnQwEBxWY03oUk+Bc3FA==",
+ "version": "7.21.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz",
+ "integrity": "sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g==",
"dev": true,
"requires": {
- "array-includes": "^3.0.3",
+ "array-includes": "^3.1.1",
+ "array.prototype.flatmap": "^1.2.3",
"doctrine": "^2.1.0",
"has": "^1.0.3",
- "jsx-ast-utils": "^2.1.0",
- "object.entries": "^1.1.0",
- "object.fromentries": "^2.0.0",
- "object.values": "^1.1.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "object.entries": "^1.1.2",
+ "object.fromentries": "^2.0.2",
+ "object.values": "^1.1.1",
"prop-types": "^15.7.2",
- "resolve": "^1.10.1"
+ "resolve": "^1.18.1",
+ "string.prototype.matchall": "^4.0.2"
},
"dependencies": {
"doctrine": {
@@ -1252,49 +1517,75 @@
"requires": {
"esutils": "^2.0.2"
}
+ },
+ "resolve": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.18.1.tgz",
+ "integrity": "sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA==",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.0.0",
+ "path-parse": "^1.0.6"
+ }
}
}
},
"eslint-plugin-standard": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz",
- "integrity": "sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-4.0.2.tgz",
+ "integrity": "sha512-nKptN8l7jksXkwFk++PhJB3cCDTcXOEyhISIN86Ue2feJ1LFyY3PrY3/xT2keXlJSY5bpmbiTG0f885/YKAvTA==",
"dev": true
},
"eslint-scope": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
- "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"requires": {
- "esrecurse": "^4.1.0",
+ "esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
}
},
"eslint-utils": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
- "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true
+ }
}
},
"eslint-visitor-keys": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
- "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
+ "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
"dev": true
},
"espree": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
- "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz",
+ "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==",
"dev": true,
"requires": {
- "acorn": "^7.1.1",
+ "acorn": "^7.4.0",
"acorn-jsx": "^5.2.0",
- "eslint-visitor-keys": "^1.1.0"
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true
+ }
}
},
"esprima": {
@@ -1312,20 +1603,28 @@
},
"dependencies": {
"estraverse": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz",
- "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+ "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true
}
}
},
"esrecurse": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
- "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"requires": {
- "estraverse": "^4.1.0"
+ "estraverse": "^5.2.0"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
+ "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
+ "dev": true
+ }
}
},
"estraverse": {
@@ -1345,6 +1644,20 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
+ "execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "requires": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
@@ -1380,6 +1693,18 @@
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
+ },
+ "dependencies": {
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ }
}
},
"extend": {
@@ -1387,17 +1712,6 @@
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
- "external-editor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
- "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
- "dev": true,
- "requires": {
- "chardet": "^0.7.0",
- "iconv-lite": "^0.4.24",
- "tmp": "^0.0.33"
- }
- },
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
@@ -1425,18 +1739,9 @@
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
},
"fecha": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz",
- "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg=="
- },
- "figures": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
- "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
- "dev": true,
- "requires": {
- "escape-string-regexp": "^1.0.5"
- }
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz",
+ "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg=="
},
"file-entry-cache": {
"version": "5.0.1",
@@ -1470,19 +1775,14 @@
"unpipe": "~1.0.0"
}
},
- "find-root": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
- "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
- "dev": true
- },
"find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
- "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"requires": {
- "locate-path": "^3.0.0"
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
}
},
"flat": {
@@ -1492,14 +1792,6 @@
"dev": true,
"requires": {
"is-buffer": "~2.0.3"
- },
- "dependencies": {
- "is-buffer": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
- "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
- "dev": true
- }
}
},
"flat-cache": {
@@ -1519,6 +1811,11 @@
"integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
"dev": true
},
+ "fn.name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
+ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
+ },
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -1544,23 +1841,30 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
+ "fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
- "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
+ "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"functional-red-black-tree": {
"version": "1.0.1",
@@ -1579,10 +1883,21 @@
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
+ "get-intrinsic": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz",
+ "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ }
+ },
"get-stdin": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz",
- "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
+ "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
"dev": true
},
"get-stream": {
@@ -1615,36 +1930,39 @@
}
},
"glob-parent": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
- "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
+ "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true
+ "version": "12.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
+ "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.8.1"
+ }
},
"got": {
- "version": "9.6.0",
- "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
- "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
- "requires": {
- "@sindresorhus/is": "^0.14.0",
- "@szmarczak/http-timer": "^1.1.2",
- "cacheable-request": "^6.0.0",
- "decompress-response": "^3.3.0",
- "duplexer3": "^0.1.4",
- "get-stream": "^4.1.0",
- "lowercase-keys": "^1.0.1",
- "mimic-response": "^1.0.1",
- "p-cancelable": "^1.0.0",
- "to-readable-stream": "^1.0.0",
- "url-parse-lax": "^3.0.0"
+ "version": "11.8.0",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.0.tgz",
+ "integrity": "sha512-k9noyoIIY9EejuhaBNLyZ31D5328LeqnyPNXJQb2XlJZcKakLqN5m6O/ikhq/0lw56kUYS54fVm+D1x57YC9oQ==",
+ "requires": {
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.1",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
}
},
"graceful-fs": {
@@ -1708,11 +2026,11 @@
"integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
},
"har-validator": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz",
- "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==",
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
+ "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"requires": {
- "ajv": "^6.5.5",
+ "ajv": "^6.12.3",
"har-schema": "^2.0.0"
}
},
@@ -1720,22 +2038,20 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"has-symbols": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
- "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
- "dev": true
+ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
},
"he": {
"version": "1.2.0",
@@ -1776,6 +2092,15 @@
"sshpk": "^1.7.0"
}
},
+ "http2-wrapper": {
+ "version": "1.0.0-beta.5.2",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz",
+ "integrity": "sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ==",
+ "requires": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ }
+ },
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -1791,9 +2116,9 @@
"dev": true
},
"import-fresh": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
- "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz",
+ "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==",
"dev": true,
"requires": {
"parent-module": "^1.0.0",
@@ -1825,83 +2150,21 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
- "inquirer": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
- "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
+ "internal-slot": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz",
+ "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==",
"dev": true,
"requires": {
- "ansi-escapes": "^3.2.0",
- "chalk": "^2.4.2",
- "cli-cursor": "^2.1.0",
- "cli-width": "^2.0.0",
- "external-editor": "^3.0.3",
- "figures": "^2.0.0",
- "lodash": "^4.17.12",
- "mute-stream": "0.0.7",
- "run-async": "^2.2.0",
- "rxjs": "^6.4.0",
- "string-width": "^2.1.0",
- "strip-ansi": "^5.1.0",
- "through": "^2.3.6"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
- "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
- "dev": true,
- "requires": {
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^4.0.0"
- },
- "dependencies": {
- "strip-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
- "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^3.0.0"
- }
- }
- }
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
- "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
- "dev": true
- }
- }
- }
+ "es-abstract": "^1.17.0-next.1",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.2"
}
},
"interpret": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
- "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw=="
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
+ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
},
"invert-kv": {
"version": "1.0.0",
@@ -1913,6 +2176,12 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
+ "is-arguments": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
+ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
+ "dev": true
+ },
"is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -1927,17 +2196,30 @@
"binary-extensions": "^2.0.0"
}
},
- "is-callable": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
- "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
+ "is-buffer": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+ "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
"dev": true
},
+ "is-callable": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
+ "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
+ },
+ "is-core-module": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz",
+ "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
"is-date-object": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
- "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
- "dev": true
+ "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
},
"is-extglob": {
"version": "2.1.1",
@@ -1962,29 +2244,43 @@
"is-extglob": "^2.1.1"
}
},
+ "is-map": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz",
+ "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==",
+ "dev": true
+ },
+ "is-negative-zero": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
+ "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
+ },
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
- "is-plain-object": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
- "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
- "requires": {
- "isobject": "^4.0.0"
- }
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
+ "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
+ "dev": true
},
"is-regex": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
- "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
- "dev": true,
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+ "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"requires": {
- "has": "^1.0.3"
+ "has-symbols": "^1.0.1"
}
},
+ "is-set": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz",
+ "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==",
+ "dev": true
+ },
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
@@ -2000,7 +2296,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
"integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
- "dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
@@ -2018,13 +2313,7 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
- "dev": true
- },
- "isobject": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
- "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA=="
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isomorphic-ws": {
"version": "4.0.1",
@@ -2036,10 +2325,26 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
+ "iterate-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz",
+ "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==",
+ "dev": true
+ },
+ "iterate-value": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz",
+ "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==",
+ "dev": true,
+ "requires": {
+ "es-get-iterator": "^1.0.2",
+ "iterate-iterator": "^1.0.1"
+ }
+ },
"jose": {
- "version": "1.27.0",
- "resolved": "https://registry.npmjs.org/jose/-/jose-1.27.0.tgz",
- "integrity": "sha512-SxYPCM9pWDaK070CXbxgL4ktVzLlE0yJxevDJtbWxv2WMQwYfpBZLYlG8PhChsiOfOXp6FrceRgTuZh1vZeDlg==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.3.tgz",
+ "integrity": "sha512-L+RlDgjO0Tk+Ki6/5IXCSEnmJCV8iMFZoBuEgu2vPQJJ4zfG/k3CAqZUMKDYNRHIDyy0QidJpOvX0NgpsAqFlw==",
"requires": {
"@panva/asn1.js": "^1.0.0"
}
@@ -2065,9 +2370,9 @@
"integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
},
"json-buffer": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
- "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
},
"json-parse-better-errors": {
"version": "1.0.2",
@@ -2091,16 +2396,20 @@
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
- "json-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-stream/-/json-stream-1.0.0.tgz",
- "integrity": "sha1-GjhU4o0rvuqzHMfd9oPS3cVlJwg="
- },
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
+ "json5": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+ "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
"jsonpath-plus": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz",
@@ -2118,73 +2427,47 @@
}
},
"jsx-ast-utils": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.3.0.tgz",
- "integrity": "sha512-3HNoc7nZ1hpZIKB3hJ7BlFRkzCx2BynRtfSwbkqZdpRdvAPsGMnzclPwrvDBS7/lalHTj21NwIeaEpysHBOudg==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz",
+ "integrity": "sha512-d4/UOjg+mxAWxCiF0c5UTSwyqbchkbqCvK87aBovhnh8GtysTjWmgC63tY0cJx/HzGgm9qnA147jVBdpOiQ2RA==",
"dev": true,
"requires": {
"array-includes": "^3.1.1",
- "object.assign": "^4.1.0"
+ "object.assign": "^4.1.1"
+ },
+ "dependencies": {
+ "object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ }
}
},
"just-extend": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz",
- "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz",
+ "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==",
"dev": true
},
"keyv": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
- "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
- "requires": {
- "json-buffer": "3.0.0"
- }
- },
- "kubernetes-client": {
- "version": "8.3.7",
- "resolved": "https://registry.npmjs.org/kubernetes-client/-/kubernetes-client-8.3.7.tgz",
- "integrity": "sha512-A0rvfQAvwAuPTooBOSErpTcnwcQxhkmawjOm/gUdGDWCUZoYmAVgVGFnc/klda+X1tvHwleavDsLqmqaYscH2w==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz",
+ "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==",
"requires": {
- "@kubernetes/client-node": "0.10.2",
- "camelcase": "^6.0.0",
- "deepmerge": "^4.2.2",
- "depd": "^2.0.0",
- "js-yaml": "^3.13.1",
- "json-stream": "^1.0.0",
- "openid-client": "^3.14.0",
- "pump": "^3.0.0",
- "qs": "^6.9.0",
- "request": "^2.88.2",
- "swagger-fluent": "^5.0.1",
- "url-join": "^4.0.1",
- "ws": "^7.2.3"
- },
- "dependencies": {
- "camelcase": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz",
- "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w=="
- },
- "depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
- },
- "qs": {
- "version": "6.9.4",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
- "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
- }
+ "json-buffer": "3.0.1"
}
},
"kuler": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz",
- "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==",
- "requires": {
- "colornames": "^1.1.1"
- }
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
+ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"lcid": {
"version": "1.0.0",
@@ -2195,13 +2478,13 @@
}
},
"levn": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
- "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"requires": {
- "prelude-ls": "~1.1.2",
- "type-check": "~0.3.2"
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
}
},
"load-json-file": {
@@ -2217,19 +2500,18 @@
}
},
"locate-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
- "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"requires": {
- "p-locate": "^3.0.0",
- "path-exists": "^3.0.0"
+ "p-locate": "^5.0.0"
}
},
"lodash": {
- "version": "4.17.19",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
- "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
+ "version": "4.17.20",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
+ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash.camelcase": {
"version": "4.3.0",
@@ -2248,22 +2530,22 @@
"dev": true
},
"log-symbols": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
- "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
+ "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
"dev": true,
"requires": {
- "chalk": "^2.4.2"
+ "chalk": "^4.0.0"
}
},
"logform": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz",
- "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz",
+ "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==",
"requires": {
"colors": "^1.2.1",
"fast-safe-stringify": "^2.0.4",
- "fecha": "^2.3.3",
+ "fecha": "^4.2.0",
"ms": "^2.1.1",
"triple-beam": "^1.3.0"
},
@@ -2290,16 +2572,16 @@
}
},
"lowercase-keys": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
- "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="
},
"lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
- "yallist": "^3.0.2"
+ "yallist": "^4.0.0"
}
},
"make-error": {
@@ -2340,12 +2622,6 @@
"mime-db": "1.44.0"
}
},
- "mimic-fn": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
- "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
- "dev": true
- },
"mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
@@ -2365,6 +2641,23 @@
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
+ "minipass": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
+ "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "requires": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ }
+ },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -2375,35 +2668,36 @@
}
},
"mocha": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz",
- "integrity": "sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==",
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz",
+ "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==",
"dev": true,
"requires": {
- "ansi-colors": "3.2.3",
+ "ansi-colors": "4.1.1",
"browser-stdout": "1.3.1",
- "chokidar": "3.3.0",
- "debug": "3.2.6",
- "diff": "3.5.0",
- "escape-string-regexp": "1.0.5",
- "find-up": "3.0.0",
- "glob": "7.1.3",
+ "chokidar": "3.4.2",
+ "debug": "4.1.1",
+ "diff": "4.0.2",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.1.6",
"growl": "1.10.5",
"he": "1.2.0",
- "js-yaml": "3.13.1",
- "log-symbols": "3.0.0",
+ "js-yaml": "3.14.0",
+ "log-symbols": "4.0.0",
"minimatch": "3.0.4",
- "mkdirp": "0.5.3",
- "ms": "2.1.1",
- "node-environment-flags": "1.0.6",
+ "ms": "2.1.2",
"object.assign": "4.1.0",
- "strip-json-comments": "2.0.1",
- "supports-color": "6.0.0",
- "which": "1.3.1",
+ "promise.allsettled": "1.0.2",
+ "serialize-javascript": "4.0.0",
+ "strip-json-comments": "3.0.1",
+ "supports-color": "7.1.0",
+ "which": "2.0.2",
"wide-align": "1.1.3",
+ "workerpool": "6.0.0",
"yargs": "13.3.2",
"yargs-parser": "13.1.2",
- "yargs-unparser": "1.6.0"
+ "yargs-unparser": "1.6.1"
},
"dependencies": {
"ansi-regex": {
@@ -2412,6 +2706,15 @@
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@@ -2424,63 +2727,58 @@
}
},
"debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
- "glob": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
- "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
- "js-yaml": {
- "version": "3.13.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
- "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
}
},
- "minimist": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
- "mkdirp": {
- "version": "0.5.3",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz",
- "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==",
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
- "minimist": "^1.2.5"
+ "p-try": "^2.0.0"
}
},
- "ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true
},
"string-width": {
@@ -2503,6 +2801,15 @@
"ansi-regex": "^4.1.0"
}
},
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
@@ -2536,6 +2843,17 @@
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ }
}
}
}
@@ -2545,24 +2863,17 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
- "mute-stream": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
- "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
- "dev": true
- },
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
},
"nats": {
- "version": "2.0.0-27",
- "resolved": "https://registry.npmjs.org/nats/-/nats-2.0.0-27.tgz",
- "integrity": "sha512-5uxxGx08/xucEJBz3c11hV36TZgUOunTSPtoJSS7GjY731WDXKE91mxdfP/7YMkLDSBSn0sDXhBkDnt87FFCtQ==",
+ "version": "2.0.0-209",
+ "resolved": "https://registry.npmjs.org/nats/-/nats-2.0.0-209.tgz",
+ "integrity": "sha512-lHYqr+wtzj2UonFnkOzfTiYhK5aVr+UYrlH7rApfR3+ZFx1vpbNLdZcGg03p++A05gzmtKGXgZfJyQ12VSTHbQ==",
"requires": {
- "nuid": "^1.1.4",
- "ts-nkeys": "^1.0.16"
+ "nkeys.js": "^1.0.0-5"
}
},
"natural-compare": {
@@ -2579,13 +2890,12 @@
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
- "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
- "dev": true
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"nise": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz",
- "integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz",
+ "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.7.0",
@@ -2612,14 +2922,25 @@
}
}
},
- "node-environment-flags": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
- "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
- "dev": true,
+ "nkeys.js": {
+ "version": "1.0.0-6",
+ "resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.0.0-6.tgz",
+ "integrity": "sha512-DctD6XECr3NYfWs2CvcwoerY6zo3pWG83JiPaLjjDLZg+CnQOd1AXYCFBxMcSEZyypHh+M7GBFgP0He8QC/ndw==",
"requires": {
- "object.getownpropertydescriptors": "^2.0.3",
- "semver": "^5.7.0"
+ "@types/node": "^14.0.26",
+ "tweetnacl": "^1.0.3"
+ },
+ "dependencies": {
+ "@types/node": {
+ "version": "14.11.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.5.tgz",
+ "integrity": "sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ=="
+ },
+ "tweetnacl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
+ }
}
},
"normalize-package-data": {
@@ -2645,10 +2966,13 @@
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz",
"integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ=="
},
- "nuid": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/nuid/-/nuid-1.1.4.tgz",
- "integrity": "sha512-PXiYyHhGfrq8H4g5HyC8enO1lz6SBe5z6x1yx/JG4tmADzDGJVQy3l1sRf3VtEvPsN8dGn9hRFRwDKWL62x0BA=="
+ "npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "requires": {
+ "path-key": "^2.0.0"
+ }
},
"number-is-nan": {
"version": "1.0.1",
@@ -2672,16 +2996,14 @@
"integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg=="
},
"object-inspect": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
- "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
- "dev": true
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
+ "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "dev": true
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"object.assign": {
"version": "4.1.0",
@@ -2704,27 +3026,6 @@
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5",
"has": "^1.0.3"
- },
- "dependencies": {
- "es-abstract": {
- "version": "1.17.5",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
- "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==",
- "dev": true,
- "requires": {
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-symbols": "^1.0.1",
- "is-callable": "^1.1.5",
- "is-regex": "^1.0.5",
- "object-inspect": "^1.7.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.0",
- "string.prototype.trimleft": "^2.1.1",
- "string.prototype.trimright": "^2.1.1"
- }
- }
}
},
"object.fromentries": {
@@ -2739,16 +3040,6 @@
"has": "^1.0.3"
}
},
- "object.getownpropertydescriptors": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
- "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.0-next.1"
- }
- },
"object.values": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
@@ -2783,30 +3074,22 @@
}
},
"one-time": {
- "version": "0.0.4",
- "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz",
- "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4="
- },
- "onetime": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
- "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
- "dev": true,
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
+ "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"requires": {
- "mimic-fn": "^1.0.0"
+ "fn.name": "1.x.x"
}
},
"openid-client": {
- "version": "3.15.1",
- "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-3.15.1.tgz",
- "integrity": "sha512-USoxzLuL08IhRiA+4z5FW25nsLgBM6lOoh+U/XWqyKJzrMbjfmVWNfof7706RgMypyvAFcAPCxPtSFqb+GpHjA==",
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-4.2.1.tgz",
+ "integrity": "sha512-07eOcJeMH3ZHNvx5DVMZQmy3vZSTQqKSSunbtM1pXb+k5LBPi5hMum1vJCFReXlo4wuLEqZ/OgbsZvXPhbGRtA==",
"requires": {
- "@types/got": "^9.6.9",
"base64url": "^3.0.1",
- "got": "^9.6.0",
- "jose": "^1.25.2",
- "lodash": "^4.17.15",
- "lru-cache": "^5.1.1",
+ "got": "^11.8.0",
+ "jose": "^2.0.2",
+ "lru-cache": "^6.0.0",
"make-error": "^1.3.6",
"object-hash": "^2.0.1",
"oidc-token-hash": "^5.0.0",
@@ -2814,17 +3097,17 @@
}
},
"optionator": {
- "version": "0.8.3",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
- "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
"dev": true,
"requires": {
- "deep-is": "~0.1.3",
- "fast-levenshtein": "~2.0.6",
- "levn": "~0.3.0",
- "prelude-ls": "~1.1.2",
- "type-check": "~0.3.2",
- "word-wrap": "~1.2.3"
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
}
},
"optjs": {
@@ -2840,12 +3123,6 @@
"lcid": "^1.0.0"
}
},
- "os-tmpdir": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
- "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
- "dev": true
- },
"p-any": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz",
@@ -2853,35 +3130,34 @@
"requires": {
"p-cancelable": "^2.0.0",
"p-some": "^5.0.0"
- },
- "dependencies": {
- "p-cancelable": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz",
- "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg=="
- }
}
},
"p-cancelable": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
- "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz",
+ "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg=="
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-limit": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
- "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz",
+ "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==",
+ "dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
- "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
- "p-limit": "^2.0.0"
+ "p-limit": "^3.0.2"
}
},
"p-some": {
@@ -2891,19 +3167,13 @@
"requires": {
"aggregate-error": "^3.0.0",
"p-cancelable": "^2.0.0"
- },
- "dependencies": {
- "p-cancelable": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz",
- "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg=="
- }
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true
},
"parent-module": {
"version": "1.0.1",
@@ -2929,9 +3199,9 @@
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"path-is-absolute": {
@@ -2942,8 +3212,7 @@
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
- "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
- "dev": true
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"path-parse": {
"version": "1.0.6",
@@ -2976,9 +3245,9 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"picomatch": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz",
- "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
+ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"pify": {
@@ -2997,6 +3266,15 @@
"load-json-file": "^5.2.0"
},
"dependencies": {
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
"load-json-file": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz",
@@ -3010,6 +3288,34 @@
"type-fest": "^0.3.0"
}
},
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@@ -3020,25 +3326,26 @@
"json-parse-better-errors": "^1.0.1"
}
},
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
"pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true
+ },
+ "type-fest": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
+ "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
+ "dev": true
}
}
},
- "pkg-config": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/pkg-config/-/pkg-config-1.1.1.tgz",
- "integrity": "sha1-VX7yLXPaPIg3EHdmxS6tq94pj+Q=",
- "dev": true,
- "requires": {
- "debug-log": "^1.0.0",
- "find-root": "^1.0.0",
- "xtend": "^4.0.1"
- }
- },
"pkg-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz",
@@ -3090,20 +3397,21 @@
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
"dev": true
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
}
}
},
"prelude-ls": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
- "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
- "prepend-http": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
- "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
- },
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -3115,6 +3423,19 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
+ "promise.allsettled": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz",
+ "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==",
+ "dev": true,
+ "requires": {
+ "array.prototype.map": "^1.0.1",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1",
+ "function-bind": "^1.1.1",
+ "iterate-value": "^1.0.0"
+ }
+ },
"prop-types": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
@@ -3127,9 +3448,9 @@
}
},
"protobufjs": {
- "version": "6.9.0",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.9.0.tgz",
- "integrity": "sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg==",
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.1.tgz",
+ "integrity": "sha512-pb8kTchL+1Ceg4lFd5XUpK8PdWacbvV5SK2ULH2ebrYtl4GjJmS24m6CKME67jzV53tbJxHlnNOSqQHbTsR9JQ==",
"requires": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
@@ -3175,9 +3496,23 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"qs": {
- "version": "6.7.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
- "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ },
+ "quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
},
"range-parser": {
"version": "1.2.1",
@@ -3264,6 +3599,12 @@
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=",
"dev": true
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
}
}
},
@@ -3278,12 +3619,12 @@
}
},
"readdirp": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
- "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
+ "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
"dev": true,
"requires": {
- "picomatch": "^2.0.4"
+ "picomatch": "^2.2.1"
}
},
"rechoir": {
@@ -3294,10 +3635,20 @@
"resolve": "^1.1.6"
}
},
+ "regexp.prototype.flags": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
+ "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.0-next.1"
+ }
+ },
"regexpp": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
- "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
+ "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
"dev": true
},
"request": {
@@ -3335,12 +3686,7 @@
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
- }
- },
- "qs": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ }
}
}
},
@@ -3352,7 +3698,8 @@
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
- "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
},
"resolve": {
"version": "1.17.0",
@@ -3362,6 +3709,11 @@
"path-parse": "^1.0.6"
}
},
+ "resolve-alpn": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.0.0.tgz",
+ "integrity": "sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA=="
+ },
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3369,23 +3721,18 @@
"dev": true
},
"responselike": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
- "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
- "requires": {
- "lowercase-keys": "^1.0.0"
- }
- },
- "restore-cursor": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
- "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz",
+ "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==",
"requires": {
- "onetime": "^2.0.0",
- "signal-exit": "^3.0.2"
+ "lowercase-keys": "^2.0.0"
}
},
+ "rfc4648": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.4.0.tgz",
+ "integrity": "sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg=="
+ },
"rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
@@ -3395,31 +3742,10 @@
"glob": "^7.1.3"
}
},
- "run-async": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
- "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
- "dev": true
- },
- "run-parallel": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz",
- "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
- "dev": true
- },
- "rxjs": {
- "version": "6.5.5",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
- "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- },
"safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safer-buffer": {
"version": "2.1.2",
@@ -3427,28 +3753,27 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"semistandard": {
- "version": "14.2.0",
- "resolved": "https://registry.npmjs.org/semistandard/-/semistandard-14.2.0.tgz",
- "integrity": "sha512-mQ0heTpbW7WWBXKOIqitlfEcAZhgGTwaHr1zzv70PnZZc53J+4u31+vLUEsh2oKVWfVgcjrykT2hz02B1Cfaaw==",
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/semistandard/-/semistandard-16.0.0.tgz",
+ "integrity": "sha512-pLETGjFyl0ETMDAEZxkC1OJBmNmPIMpMkayStGTgHMMh/5FM7Rbk5NWc1t7yfQ4PrRURQH8MUg3ZxvojJJifcw==",
"dev": true,
"requires": {
- "eslint": "~6.4.0",
- "eslint-config-semistandard": "15.0.0",
- "eslint-config-standard": "14.1.0",
- "eslint-config-standard-jsx": "8.1.0",
- "eslint-plugin-import": "~2.18.0",
- "eslint-plugin-node": "~10.0.0",
+ "eslint": "~7.12.1",
+ "eslint-config-semistandard": "15.0.1",
+ "eslint-config-standard": "16.0.0",
+ "eslint-config-standard-jsx": "10.0.0",
+ "eslint-plugin-import": "~2.22.1",
+ "eslint-plugin-node": "~11.1.0",
"eslint-plugin-promise": "~4.2.1",
- "eslint-plugin-react": "~7.14.2",
- "eslint-plugin-standard": "~4.0.0",
- "standard-engine": "^12.0.0"
+ "eslint-plugin-react": "~7.21.5",
+ "eslint-plugin-standard": "~4.0.2",
+ "standard-engine": "^14.0.0"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"send": {
"version": "0.17.1",
@@ -3477,6 +3802,15 @@
}
}
},
+ "serialize-javascript": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
+ "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
@@ -3491,7 +3825,8 @@
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+ "dev": true
},
"setprototypeof": {
"version": "1.1.1",
@@ -3502,7 +3837,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
- "dev": true,
"requires": {
"shebang-regex": "^1.0.0"
}
@@ -3510,8 +3844,7 @@
"shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
- "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
- "dev": true
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
},
"shelljs": {
"version": "0.8.4",
@@ -3523,11 +3856,54 @@
"rechoir": "^0.6.2"
}
},
+ "side-channel": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.3.tgz",
+ "integrity": "sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==",
+ "dev": true,
+ "requires": {
+ "es-abstract": "^1.18.0-next.0",
+ "object-inspect": "^1.8.0"
+ },
+ "dependencies": {
+ "es-abstract": {
+ "version": "1.18.0-next.1",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+ "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+ "dev": true,
+ "requires": {
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1",
+ "is-callable": "^1.2.2",
+ "is-negative-zero": "^2.0.0",
+ "is-regex": "^1.1.1",
+ "object-inspect": "^1.8.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.1",
+ "string.prototype.trimend": "^1.0.1",
+ "string.prototype.trimstart": "^1.0.1"
+ }
+ },
+ "object.assign": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
+ "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ }
+ }
+ },
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
- "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
- "dev": true
+ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"simple-swizzle": {
"version": "0.2.2",
@@ -3538,41 +3914,18 @@
}
},
"sinon": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.1.tgz",
- "integrity": "sha512-iTTyiQo5T94jrOx7X7QLBZyucUJ2WvL9J13+96HMfm2CGoJYbIPqRfl6wgNcqmzk0DI28jeGx5bUTXizkrqBmg==",
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.1.0.tgz",
+ "integrity": "sha512-9zQShgaeylYH6qtsnNXlTvv0FGTTckuDfHBi+qhgj5PvW2r2WslHZpgc3uy3e/ZAoPkqaOASPi+juU6EdYRYxA==",
"dev": true,
"requires": {
- "@sinonjs/commons": "^1.7.0",
- "@sinonjs/fake-timers": "^6.0.0",
+ "@sinonjs/commons": "^1.7.2",
+ "@sinonjs/fake-timers": "^6.0.1",
"@sinonjs/formatio": "^5.0.1",
- "@sinonjs/samsam": "^5.0.3",
+ "@sinonjs/samsam": "^5.1.0",
"diff": "^4.0.2",
- "nise": "^4.0.1",
+ "nise": "^4.0.4",
"supports-color": "^7.1.0"
- },
- "dependencies": {
- "diff": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
- "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
- "dev": true
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true
- },
- "supports-color": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
- "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
- "dev": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- }
}
},
"sleep-promise": {
@@ -3591,6 +3944,15 @@
"is-fullwidth-code-point": "^2.0.0"
},
"dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
@@ -3599,6 +3961,22 @@
}
}
},
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true
+ },
+ "source-map-support": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
+ "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
"spdx-correct": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz",
@@ -3626,9 +4004,9 @@
}
},
"spdx-license-ids": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz",
- "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz",
+ "integrity": "sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==",
"dev": true
},
"sprintf-js": {
@@ -3658,15 +4036,15 @@
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
},
"standard-engine": {
- "version": "12.1.0",
- "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-12.1.0.tgz",
- "integrity": "sha512-DVJnWM1CGkag4ucFLGdiYWa5/kJURPONmMmk17p8FT5NE4UnPZB1vxWnXnRo2sPSL78pWJG8xEM+1Tu19z0deg==",
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/standard-engine/-/standard-engine-14.0.1.tgz",
+ "integrity": "sha512-7FEzDwmHDOGva7r9ifOzD3BGdTbA7ujJ50afLVdW/tK14zQEptJjbFuUfn50irqdHDcTbNh0DTIoMPynMCXb0Q==",
"dev": true,
"requires": {
- "deglob": "^4.0.1",
- "get-stdin": "^7.0.0",
+ "get-stdin": "^8.0.0",
"minimist": "^1.2.5",
- "pkg-conf": "^3.1.0"
+ "pkg-conf": "^3.1.0",
+ "xdg-basedir": "^4.0.0"
}
},
"statuses": {
@@ -3674,6 +4052,11 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
+ "stream-buffers": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz",
+ "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ=="
+ },
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -3684,24 +4067,36 @@
"strip-ansi": "^3.0.0"
}
},
- "string.prototype.trimleft": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz",
- "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==",
+ "string.prototype.matchall": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz",
+ "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
- "function-bind": "^1.1.1"
+ "es-abstract": "^1.17.0",
+ "has-symbols": "^1.0.1",
+ "internal-slot": "^1.0.2",
+ "regexp.prototype.flags": "^1.3.0",
+ "side-channel": "^1.0.2"
}
},
- "string.prototype.trimright": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz",
- "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==",
- "dev": true,
+ "string.prototype.trimend": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
+ "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
"requires": {
"define-properties": "^1.1.3",
- "function-bind": "^1.1.1"
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
+ "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
}
},
"string_decoder": {
@@ -3710,13 +4105,6 @@
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"requires": {
"safe-buffer": "~5.2.0"
- },
- "dependencies": {
- "safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
- }
}
},
"strip-ansi": {
@@ -3733,29 +4121,24 @@
"integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
"dev": true
},
+ "strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
+ },
"strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
+ "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
"dev": true
},
"supports-color": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
- "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+ "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
"dev": true,
"requires": {
- "has-flag": "^3.0.0"
- }
- },
- "swagger-fluent": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/swagger-fluent/-/swagger-fluent-5.0.3.tgz",
- "integrity": "sha512-i43ADMtPi7dxAN75Lw50SlncMB31FgaVwXqKioR8SWs+Yon2RbiLU1J1PGMXA4N8cSt9Vz5RHzaoKjz/+iW88g==",
- "requires": {
- "deepmerge": "^4.2.2",
- "is-plain-object": "^3.0.0",
- "request": "^2.88.0"
+ "has-flag": "^4.0.0"
}
},
"table": {
@@ -3804,6 +4187,26 @@
}
}
},
+ "tar": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.5.tgz",
+ "integrity": "sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==",
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "dependencies": {
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
+ }
+ }
+ },
"text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -3815,25 +4218,31 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
- "through": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
- "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
- "dev": true
- },
"tmp": {
- "version": "0.0.33",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
- "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
- "dev": true,
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+ "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
"requires": {
- "os-tmpdir": "~1.0.2"
+ "rimraf": "^3.0.0"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
}
},
- "to-readable-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
- "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q=="
+ "tmp-promise": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz",
+ "integrity": "sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA==",
+ "requires": {
+ "tmp": "^0.2.0"
+ }
},
"to-regex-range": {
"version": "5.0.1",
@@ -3863,25 +4272,22 @@
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
- "ts-nkeys": {
- "version": "1.0.16",
- "resolved": "https://registry.npmjs.org/ts-nkeys/-/ts-nkeys-1.0.16.tgz",
- "integrity": "sha512-1qrhAlavbm36wtW+7NtKOgxpzl+70NTF8xlz9mEhiA5zHMlMxjj3sEVKWm3pGZhHXE0Q3ykjrj+OSRVaYw+Dqg==",
+ "tsconfig-paths": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz",
+ "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==",
+ "dev": true,
"requires": {
- "tweetnacl": "^1.0.3"
- },
- "dependencies": {
- "tweetnacl": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
- "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
- }
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.1",
+ "minimist": "^1.2.0",
+ "strip-bom": "^3.0.0"
}
},
"tslib": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
- "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"tunnel-agent": {
"version": "0.6.0",
@@ -3897,12 +4303,12 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
},
"type-check": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
- "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"requires": {
- "prelude-ls": "~1.1.2"
+ "prelude-ls": "^1.2.1"
}
},
"type-detect": {
@@ -3912,9 +4318,9 @@
"dev": true
},
"type-fest": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
- "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
"dev": true
},
"type-is": {
@@ -3927,21 +4333,15 @@
}
},
"typescript": {
- "version": "3.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz",
- "integrity": "sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==",
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz",
+ "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==",
"dev": true
},
"underscore": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
- "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg=="
- },
- "uniq": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
- "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
- "dev": true
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz",
+ "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw=="
},
"unpipe": {
"version": "1.0.0",
@@ -3956,19 +4356,6 @@
"punycode": "^2.1.0"
}
},
- "url-join": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
- "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="
- },
- "url-parse-lax": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
- "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
- "requires": {
- "prepend-http": "^2.0.0"
- }
- },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3985,9 +4372,9 @@
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"v8-compile-cache": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz",
- "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz",
+ "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==",
"dev": true
},
"validate-npm-package-license": {
@@ -4019,7 +4406,6 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "dev": true,
"requires": {
"isexe": "^2.0.0"
}
@@ -4027,7 +4413,8 @@
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
- "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
},
"wide-align": {
"version": "1.1.3",
@@ -4044,27 +4431,34 @@
"integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY="
},
"winston": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz",
- "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==",
- "requires": {
- "async": "^2.6.1",
- "diagnostics": "^1.1.1",
- "is-stream": "^1.1.0",
- "logform": "^2.1.1",
- "one-time": "0.0.4",
- "readable-stream": "^3.1.1",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
+ "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
+ "requires": {
+ "@dabh/diagnostics": "^2.0.2",
+ "async": "^3.1.0",
+ "is-stream": "^2.0.0",
+ "logform": "^2.2.0",
+ "one-time": "^1.0.0",
+ "readable-stream": "^3.4.0",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
- "winston-transport": "^4.3.0"
+ "winston-transport": "^4.4.0"
+ },
+ "dependencies": {
+ "is-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
+ "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
+ }
}
},
"winston-transport": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz",
- "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz",
+ "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==",
"requires": {
- "readable-stream": "^2.3.6",
+ "readable-stream": "^2.3.7",
"triple-beam": "^1.2.0"
},
"dependencies": {
@@ -4082,6 +4476,11 @@
"util-deprecate": "~1.0.1"
}
},
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -4098,6 +4497,12 @@
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true
},
+ "workerpool": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz",
+ "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==",
+ "dev": true
+ },
"wrap-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
@@ -4122,20 +4527,20 @@
}
},
"ws": {
- "version": "7.3.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
- "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
+ "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ=="
},
"wtfnode": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.1.tgz",
- "integrity": "sha512-S7S7D8CGHVCtlTn1IWX+nEbxavpL9+fk3vk02RPZHiExyZFb9oKTTig3nEnMCL2yaJ4047V5lAkuulXuO2OsOw==",
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.3.tgz",
+ "integrity": "sha512-Ll7iH8MbRQTE+QTw20Xax/0PM5VeSVSOhsmoR3+knWuJkEWTV5d9yPO6Sb+IDbt9I4UCrKpvHuF9T9zteRNOuA==",
"dev": true
},
- "xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "xdg-basedir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
+ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
"dev": true
},
"y18n": {
@@ -4144,26 +4549,22 @@
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
},
"yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yargs": {
- "version": "15.3.1",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
- "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==",
+ "version": "16.0.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.0.3.tgz",
+ "integrity": "sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA==",
"requires": {
- "cliui": "^6.0.0",
- "decamelize": "^1.2.0",
- "find-up": "^4.1.0",
- "get-caller-file": "^2.0.1",
+ "cliui": "^7.0.0",
+ "escalade": "^3.0.2",
+ "get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
- "require-main-filename": "^2.0.0",
- "set-blocking": "^2.0.0",
"string-width": "^4.2.0",
- "which-module": "^2.0.0",
- "y18n": "^4.0.0",
- "yargs-parser": "^18.1.1"
+ "y18n": "^5.0.1",
+ "yargs-parser": "^20.0.0"
},
"dependencies": {
"ansi-regex": {
@@ -4172,27 +4573,21 @@
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"ansi-styles": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
- "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
- "@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
- },
"cliui": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
- "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.1.tgz",
+ "integrity": "sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
- "wrap-ansi": "^6.2.0"
+ "wrap-ansi": "^7.0.0"
}
},
"color-convert": {
@@ -4213,41 +4608,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
- "find-up": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
- "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
- "requires": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- }
- },
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
- "locate-path": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
- "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
- "requires": {
- "p-locate": "^4.1.0"
- }
- },
- "p-locate": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
- "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
- "requires": {
- "p-limit": "^2.2.0"
- }
- },
- "path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
- },
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
@@ -4267,9 +4632,9 @@
}
},
"wrap-ansi": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
- "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -4277,18 +4642,14 @@
}
},
"y18n": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
- "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.2.tgz",
+ "integrity": "sha512-CkwaeZw6dQgqgPGeTWKMXCRmMcBgETFlTml1+ZOO+q7kGst8NREJ+eWwFNPVUQ4QGdAaklbqCZHH6Zuep1RjiA=="
},
"yargs-parser": {
- "version": "18.1.3",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
- "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
- "requires": {
- "camelcase": "^5.0.0",
- "decamelize": "^1.2.0"
- }
+ "version": "20.2.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.1.tgz",
+ "integrity": "sha512-yYsjuSkjbLMBp16eaOt7/siKTjNVjMm3SoJnIg3sEh/JsvqVVDyjRKmaJV4cl+lNIgq6QEco2i3gDebJl7/vLA=="
}
}
},
@@ -4311,14 +4672,16 @@
}
},
"yargs-unparser": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
- "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz",
+ "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==",
"dev": true,
"requires": {
+ "camelcase": "^5.3.1",
+ "decamelize": "^1.2.0",
"flat": "^4.1.0",
- "lodash": "^4.17.15",
- "yargs": "^13.3.0"
+ "is-plain-obj": "^1.1.0",
+ "yargs": "^14.2.3"
},
"dependencies": {
"ansi-regex": {
@@ -4327,6 +4690,21 @@
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ },
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
@@ -4338,12 +4716,55 @@
"wrap-ansi": "^5.1.0"
}
},
+ "find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
+ "locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+ "dev": true
+ },
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
@@ -4382,12 +4803,13 @@
"dev": true
},
"yargs": {
- "version": "13.3.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
- "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",
+ "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
+ "decamelize": "^1.2.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
@@ -4396,7 +4818,17 @@
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
- "yargs-parser": "^13.1.2"
+ "yargs-parser": "^15.0.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "15.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz",
+ "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
}
}
}
diff --git a/csi/moac/package.json b/csi/moac/package.json
index fca408ba8..c6c9fc6de 100644
--- a/csi/moac/package.json
+++ b/csi/moac/package.json
@@ -15,7 +15,7 @@
"scripts": {
"prepare": "./bundle_protos.sh",
"clean": "rm -f replica.js pool.js nexus.js",
- "purge": "rm -rf node_modules proto replica.js pool.js nexus.js",
+ "purge": "rm -rf node_modules proto replica.js pool.js nexus.js watcher.js node_operator.js pool_operator.js volume_operator.js",
"compile": "tsc --pretty",
"start": "./index.js",
"test": "mocha test/index.js",
@@ -24,26 +24,28 @@
},
"license": "ISC",
"dependencies": {
- "@grpc/proto-loader": "^0.5.3",
+ "@grpc/proto-loader": "^0.5.5",
+ "@types/lodash": "^4.14.161",
+ "client-node-fixed-watcher": "^0.13.2",
"express": "^4.17.1",
"grpc-promise": "^1.4.0",
"grpc-uds": "^0.1.4",
- "js-yaml": "^3.13.1",
- "kubernetes-client": "^8.3.6",
- "lodash": "^4.17.19",
+ "js-yaml": "^3.14.0",
+ "lodash": "^4.17.20",
"nats": "^2.0.0-27",
"sleep-promise": "^8.0.1",
- "winston": "^3.2.1",
- "yargs": "^15.3.1"
+ "winston": "^3.3.3",
+ "yargs": "^16.0.3"
},
"devDependencies": {
"chai": "^4.2.0",
"dirty-chai": "^2.0.1",
- "mocha": "^7.1.1",
- "semistandard": "^14.2.0",
- "sinon": "^9.0.1",
- "typescript": "^3.9.3",
- "wtfnode": "^0.8.1"
+ "mocha": "^8.1.3",
+ "semistandard": "^16.0.0",
+ "sinon": "^9.1.0",
+ "source-map-support": "^0.5.19",
+ "typescript": "^4.0.3",
+ "wtfnode": "^0.8.3"
},
"files": [
"*.js",
diff --git a/csi/moac/pool_operator.js b/csi/moac/pool_operator.js
deleted file mode 100644
index 3ea9d21cd..000000000
--- a/csi/moac/pool_operator.js
+++ /dev/null
@@ -1,427 +0,0 @@
-// Pool operator monitors k8s pool resources (desired state). It creates
-// and destroys pools on storage nodes to reflect the desired state.
-
-'use strict';
-
-const _ = require('lodash');
-const path = require('path');
-const fs = require('fs');
-const yaml = require('js-yaml');
-const log = require('./logger').Logger('pool-operator');
-const Watcher = require('./watcher');
-const EventStream = require('./event_stream');
-const Workq = require('./workq');
-const { FinalizerHelper } = require('./finalizer_helper');
-const poolFinalizerValue = 'finalizer.mayastor.openebs.io';
-
-// Load custom resource definition
-const crdPool = yaml.safeLoad(
- fs.readFileSync(path.join(__dirname, '/crds/mayastorpool.yaml'), 'utf8')
-);
-
-// Pool operator tries to bring the real state of storage pools on mayastor
-// nodes in sync with mayastorpool custom resources in k8s.
-class PoolOperator {
- constructor (namespace) {
- this.namespace = namespace;
- this.k8sClient = null; // k8s client
- this.registry = null; // registry containing info about mayastor nodes
- this.eventStream = null; // A stream of node and pool events.
- this.resource = {}; // List of storage pool resources indexed by name.
- this.watcher = null; // pool CRD watcher.
- this.workq = new Workq(); // for serializing pool operations
- this.finalizerHelper = new FinalizerHelper(
- this.namespace,
- crdPool.spec.group,
- crdPool.spec.version,
- crdPool.spec.names.plural
- );
- }
-
- // Create pool CRD if it doesn't exist and augment client object so that CRD
- // can be manipulated as any other standard k8s api object.
- // Bind node operator to pool operator through events.
- //
- // @param {object} k8sClient Client for k8s api server.
- // @param {object} registry Registry with node and pool information.
- //
- async init (k8sClient, registry) {
- log.info('Initializing pool operator');
-
- try {
- await k8sClient.apis[
- 'apiextensions.k8s.io'
- ].v1beta1.customresourcedefinitions.post({ body: crdPool });
- log.info('Created CRD ' + crdPool.spec.names.kind);
- } catch (err) {
- // API returns a 409 Conflict if CRD already exists.
- if (err.statusCode !== 409) throw err;
- }
- k8sClient.addCustomResourceDefinition(crdPool);
-
- this.k8sClient = k8sClient;
- this.registry = registry;
- this.watcher = new Watcher(
- 'pool',
- this.k8sClient.apis['openebs.io'].v1alpha1.namespaces(
- this.namespace
- ).mayastorpools,
- this.k8sClient.apis['openebs.io'].v1alpha1.watch.namespaces(
- this.namespace
- ).mayastorpools,
- this._filterMayastorPool
- );
- }
-
- // Convert pool CRD to an object with specification of the pool.
- //
- // @param {object} msp MayaStor pool custom resource.
- // @returns {object} Pool properties defining a pool.
- //
- _filterMayastorPool (msp) {
- const props = {
- name: msp.metadata.name,
- node: msp.spec.node,
- disks: msp.spec.disks
- };
- // sort the disks for easy string to string comparison
- props.disks.sort();
- return props;
- }
-
- // Start pool operator's watcher loop.
- //
- // NOTE: Not getting the start sequence right can have catastrophic
- // consequence leading to unintended pool destruction and data loss
- // (i.e. when node info is available before the pool CRD is).
- //
- // The right order of steps is:
- // 1. Get pool resources
- // 2. Get info about pools on storage nodes
- async start () {
- var self = this;
-
- // get pool k8s resources for initial synchronization and install
- // event handlers to follow changes to them.
- await self.watcher.start();
- self._bindWatcher(self.watcher);
- self.watcher.list().forEach((r) => {
- const poolName = r.name;
- log.debug(`Reading pool custom resource "${poolName}"`);
- self.resource[poolName] = r;
- });
-
- // this will start async processing of node and pool events
- self.eventStream = new EventStream({ registry: self.registry });
- self.eventStream.on('data', async (ev) => {
- if (ev.kind === 'pool') {
- await self.workq.push(ev, self._onPoolEvent.bind(self));
- } else if (ev.kind === 'node' && (ev.eventType === 'sync' || ev.eventType === 'mod')) {
- await self.workq.push(ev.object.name, self._onNodeSyncEvent.bind(self));
- } else if (ev.kind === 'replica' && (ev.eventType === 'new' || ev.eventType === 'del')) {
- await self.workq.push(ev, self._onReplicaEvent.bind(self));
- }
- });
- }
-
- // Handler for new/mod/del pool events
- //
- // @param {object} ev Pool event as received from event stream.
- //
- async _onPoolEvent (ev) {
- const name = ev.object.name;
- const resource = this.resource[name];
-
- log.debug(`Received "${ev.eventType}" event for pool "${name}"`);
-
- if (ev.eventType === 'new') {
- if (!resource) {
- log.warn(`Unknown pool "${name}" will be destroyed`);
- await this._destroyPool(name);
- } else {
- await this._updateResource(ev.object);
- }
- } else if (ev.eventType === 'mod') {
- await this._updateResource(ev.object);
- } else if (ev.eventType === 'del' && resource) {
- log.warn(`Recreating destroyed pool "${name}"`);
- await this._createPool(resource);
- }
- }
-
- // Handler for node sync event.
- //
- // Either the node is new or came up after an outage - check that we
- // don't have any pending pools waiting to be created on it.
- //
- // @param {string} nodeName Name of the new node.
- //
- async _onNodeSyncEvent (nodeName) {
- log.debug(`Syncing pool records for node "${nodeName}"`);
-
- const resources = Object.values(this.resource).filter(
- (ent) => ent.node === nodeName
- );
- for (let i = 0; i < resources.length; i++) {
- await this._createPool(resources[i]);
- }
- }
-
- // Handler for new/del replica events
- //
- // @param {object} ev Replica event as received from event stream.
- //
- async _onReplicaEvent (ev) {
- const replica = ev.object;
-
- log.debug(`Received "${ev.eventType}" event for replica "${replica.name}"`);
-
- if (replica.pool === undefined) {
- log.warn(`not processing for finalizers: pool not defined for replica ${replica.name}.`);
- return;
- }
-
- const pool = this.registry.getPool(replica.pool.name);
- if (pool == null) {
- log.warn(`not processing for finalizers: failed to retrieve pool ${replica.pool.name}`);
- return;
- }
-
- log.debug(`On "${ev.eventType}" event for replica "${replica.name}", replica count=${pool.replicas.length}`);
-
- if (pool.replicas.length > 0) {
- this.finalizerHelper.addFinalizerToCR(replica.pool.name, poolFinalizerValue);
- } else {
- this.finalizerHelper.removeFinalizerFromCR(replica.pool.name, poolFinalizerValue);
- }
- }
-
- // Stop the watcher, destroy event stream and reset resource cache.
- async stop () {
- this.watcher.removeAllListeners();
- await this.watcher.stop();
- this.eventStream.destroy();
- this.eventStream = null;
- this.resource = {};
- }
-
- // Bind watcher's new/mod/del events to pool operator's callbacks.
- //
- // @param {object} watcher k8s pool resource watcher.
- //
- _bindWatcher (watcher) {
- var self = this;
- watcher.on('new', (resource) => {
- self.workq.push(resource, self._createPool.bind(self));
- });
- watcher.on('mod', (resource) => {
- self.workq.push(resource, self._modifyPool.bind(self));
- });
- watcher.on('del', (resource) => {
- self.workq.push(resource.name, self._destroyPool.bind(self));
- });
- }
-
- // Create a pool according to the specification.
- // That includes parameters checks, node lookup and a call to registry
- // to create the pool.
- //
- // @param {object} resource Pool resource properties.
- // @param {string} resource.name Pool name.
- // @param {string} resource.node Node name for the pool.
- // @param {string[]} resource.disks Disks comprising the pool.
- //
- async _createPool (resource) {
- const name = resource.name;
- const nodeName = resource.node;
- this.resource[name] = resource;
-
- let pool = this.registry.getPool(name);
- if (pool) {
- // the pool already exists, just update its properties in k8s
- await this._updateResource(pool);
- return;
- }
-
- const node = this.registry.getNode(nodeName);
- if (!node) {
- const msg = `mayastor does not run on node "${nodeName}"`;
- log.error(`Cannot create pool "${name}": ${msg}`);
- await this._updateResourceProps(name, 'pending', msg);
- return;
- }
- if (!node.isSynced()) {
- log.debug(
- `The pool "${name}" will be synced when the node "${nodeName}" is synced`
- );
- return;
- }
-
- // We will update the pool status once the pool is created, but
- // that can take a time, so set reasonable default now.
- await this._updateResourceProps(name, 'pending', 'Creating the pool');
-
- try {
- // pool resource props will be updated when "new" pool event is emitted
- pool = await node.createPool(name, resource.disks);
- } catch (err) {
- log.error(`Failed to create pool "${name}": ${err}`);
- await this._updateResourceProps(name, 'pending', err.toString());
- }
- }
-
- // Remove the pool from internal state and if it exists destroy it.
- // Does not throw - only logs an error.
- //
- // @param {string} name Name of the pool to destroy.
- //
- async _destroyPool (name) {
- var resource = this.resource[name];
- var pool = this.registry.getPool(name);
-
- if (resource) {
- delete this.resource[name];
- }
- if (pool) {
- try {
- await pool.destroy();
- } catch (err) {
- log.error(`Failed to destroy pool "${name}@${pool.node.name}": ${err}`);
- }
- }
- }
-
- // Changing pool parameters is actually not supported. However the pool
- // operator's state should reflect the k8s state, so we make the change
- // only at operator level and log a warning message.
- //
- // @param {string} newPool New pool parameters.
- //
- async _modifyPool (newProps) {
- const name = newProps.name;
- const curProps = this.resource[name];
- if (!curProps) {
- log.warn(`Ignoring modification to unknown pool "${name}"`);
- return;
- }
- if (!_.isEqual(curProps.disks, newProps.disks)) {
- // TODO: Growing pools, mirrors, etc. is currently unsupported.
- log.error(`Changing disks of the pool "${name}" is not supported`);
- curProps.disks = newProps.disks;
- }
- // Changing node implies destroying the pool on the old node and recreating
- // it on the new node that is destructive action -> unsupported.
- if (curProps.node !== newProps.node) {
- log.error(`Moving pool "${name}" between nodes is not supported`);
- curProps.node = newProps.node;
- }
- }
-
- // Update status properties of k8s resource to be aligned with pool object
- // properties.
- //
- // NOTE: This method does not throw if the update fails as there is nothing
- // we can do if it fails. Though it logs an error message.
- //
- // @param {object} pool Pool object.
- //
- async _updateResource (pool) {
- var name = pool.name;
- var resource = this.resource[name];
-
- // we don't track this pool so we cannot update the CRD
- if (!resource) {
- log.warn(`State of unknown pool "${name}" has changed`);
- return;
- }
- var state = pool.state.replace(/^POOL_/, '').toLowerCase();
- var reason = '';
- if (state === 'offline') {
- reason = `mayastor does not run on the node "${pool.node}"`;
- }
-
- await this._updateResourceProps(
- name,
- state,
- reason,
- pool.disks,
- pool.capacity,
- pool.used,
- pool.replicas.length
- );
- }
-
- // Update status properties of k8s CRD object.
- //
- // Parameters "name" and "state" are required, the rest is optional.
- //
- // NOTE: This method does not throw if the update fails as there is nothing
- // we can do if it fails. Though we log an error message in such a case.
- //
- // @param {string} name Name of the pool.
- // @param {string} state State of the pool.
- // @param {string} [reason] Reason describing the root cause of the state.
- // @param {string[]} [disks] Disk URIs.
- // @param {number} [capacity] Capacity of the pool in bytes.
- // @param {number} [used] Used bytes in the pool.
- // @param {number} [replicacount] Count of replicas using the pool.
- //
- async _updateResourceProps (name, state, reason, disks, capacity, used, replicacount) {
- // For the update of CRD status we need a real k8s pool object, change the
- // status in it and store it back. Another reason for grabbing the latest
- // version of CRD from watcher cache (even if this.resource contains an older
- // version than the one fetched from watcher cache) is that k8s refuses to
- // update CRD unless the object's resourceVersion is the latest.
- var k8sPool = this.watcher.getRaw(name);
-
- // it could happen that the object was deleted in the meantime
- if (!k8sPool) {
- log.warn(
- `Pool resource "${name}" was deleted before its status could be updated`
- );
- return;
- }
- const status = k8sPool.status || {};
- // avoid the update if the object has not changed
- if (
- state === status.state &&
- reason === status.reason &&
- capacity === status.capacity &&
- used === status.used &&
- _.isEqual(disks, status.disks)
- ) {
- return;
- }
-
- log.debug(`Updating properties of pool resource "${name}"`);
- status.state = state;
- status.reason = reason || '';
- status.disks = disks || [];
- if (capacity != null) {
- status.capacity = capacity;
- }
- if (used != null) {
- status.used = used;
- }
-
- k8sPool.status = status;
- try {
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastorpools(name)
- .status.put({ body: k8sPool });
- } catch (err) {
- log.error(`Failed to update status of pool "${name}": ${err}`);
- }
-
- if (replicacount != null) {
- if (replicacount === 0) {
- this.finalizerHelper.removeFinalizer(k8sPool, name, poolFinalizerValue);
- } else {
- this.finalizerHelper.addFinalizer(k8sPool, name, poolFinalizerValue);
- }
- }
- }
-}
-
-module.exports = PoolOperator;
diff --git a/csi/moac/pool_operator.ts b/csi/moac/pool_operator.ts
new file mode 100644
index 000000000..ae67d7714
--- /dev/null
+++ b/csi/moac/pool_operator.ts
@@ -0,0 +1,476 @@
+// Pool operator monitors k8s pool resources (desired state). It creates
+// and destroys pools on storage nodes to reflect the desired state.
+
+import * as fs from 'fs';
+import * as _ from 'lodash';
+import * as path from 'path';
+import {
+ ApiextensionsV1Api,
+ KubeConfig,
+} from 'client-node-fixed-watcher';
+import {
+ CustomResource,
+ CustomResourceCache,
+ CustomResourceMeta,
+} from './watcher';
+
+const yaml = require('js-yaml');
+const log = require('./logger').Logger('pool-operator');
+const EventStream = require('./event_stream');
+const Workq = require('./workq');
+
+const RESOURCE_NAME: string = 'mayastorpool';
+const POOL_FINALIZER = 'finalizer.mayastor.openebs.io';
+
+// Load custom resource definition
+const crdPool = yaml.safeLoad(
+ fs.readFileSync(path.join(__dirname, '/crds/mayastorpool.yaml'), 'utf8')
+);
+
+// Set of possible pool states. Some of them come from mayastor and
+// offline, pending and error are deduced in the control plane itself.
+enum PoolState {
+ Unknown = "unknown",
+ Online = "online",
+ Degraded = "degraded",
+ Faulted = "faulted",
+ Offline = "offline",
+ Pending = "pending",
+ Error = "error",
+}
+
+function poolStateFromString(val: string): PoolState {
+ if (val === PoolState.Online) {
+ return PoolState.Online;
+ } else if (val === PoolState.Degraded) {
+ return PoolState.Degraded;
+ } else if (val === PoolState.Faulted) {
+ return PoolState.Faulted;
+ } else if (val === PoolState.Offline) {
+ return PoolState.Offline;
+ } else if (val === PoolState.Pending) {
+ return PoolState.Pending;
+ } else if (val === PoolState.Error) {
+ return PoolState.Error;
+ } else {
+ return PoolState.Unknown;
+ }
+}
+
+// Object defines properties of pool resource.
+export class PoolResource extends CustomResource {
+ apiVersion?: string;
+ kind?: string;
+ metadata: CustomResourceMeta;
+ spec: {
+ node: string,
+ disks: string[],
+ };
+ status: {
+ state: string,
+ reason?: string,
+ disks?: string[],
+ capacity?: number,
+ used?: number
+ };
+
+ // Create and validate pool custom resource.
+ constructor(cr: CustomResource) {
+ super();
+ this.apiVersion = cr.apiVersion;
+ this.kind = cr.kind;
+ if (cr.metadata === undefined) {
+ throw new Error('missing metadata');
+ } else {
+ this.metadata = cr.metadata;
+ }
+ if (cr.spec === undefined) {
+ throw new Error('missing spec');
+ } else {
+ let node = (cr.spec as any).node;
+ if (typeof node !== 'string') {
+ throw new Error('missing or invalid node in spec');
+ }
+ let disks = (cr.spec as any).disks;
+ if (!Array.isArray(disks)) {
+ throw new Error('missing or invalid disks in spec');
+ }
+ disks = disks.slice(0).sort();
+ //if (typeof disks !== 'string') {
+ this.spec = { node, disks };
+ }
+ this.status = {
+ state: poolStateFromString(cr.status?.state),
+ reason: cr.status?.reason,
+ disks: cr.status?.disks,
+ capacity: cr.status?.capacity,
+ used: cr.status?.used,
+ };
+ }
+
+ // Extract name of the pool from the resource metadata.
+ getName(): string {
+ if (this.metadata.name === undefined) {
+ throw Error("Resource object does not have a name")
+ } else {
+ return this.metadata.name;
+ }
+ }
+}
+
+// Pool operator tries to bring the real state of storage pools on mayastor
+// nodes in sync with mayastorpool custom resources in k8s.
+export class PoolOperator {
+ namespace: string;
+ watcher: CustomResourceCache; // k8s resource watcher for pools
+ registry: any; // registry containing info about mayastor nodes
+ eventStream: any; // A stream of node and pool events.
+ workq: any; // for serializing pool operations
+
+ // Create pool operator.
+ //
+ // @param namespace Namespace the operator should operate on.
+ // @param kubeConfig KubeConfig.
+ // @param registry Registry with node objects.
+ // @param [idleTimeout] Timeout for restarting watcher connection when idle.
+ constructor (
+ namespace: string,
+ kubeConfig: KubeConfig,
+ registry: any,
+ idleTimeout: number | undefined,
+ ) {
+ this.namespace = namespace;
+ this.registry = registry; // registry containing info about mayastor nodes
+ this.eventStream = null; // A stream of node and pool events.
+ this.workq = new Workq(); // for serializing pool operations
+ this.watcher = new CustomResourceCache(
+ this.namespace,
+ RESOURCE_NAME,
+ kubeConfig,
+ PoolResource,
+ { idleTimeout }
+ );
+ }
+
+ // Create pool CRD if it doesn't exist.
+ //
+ // @param kubeConfig KubeConfig.
+ async init (kubeConfig: KubeConfig) {
+ log.info('Initializing pool operator');
+ let k8sExtApi = kubeConfig.makeApiClient(ApiextensionsV1Api);
+ try {
+ await k8sExtApi.createCustomResourceDefinition(crdPool);
+ log.info(`Created CRD ${RESOURCE_NAME}`);
+ } catch (err) {
+ // API returns a 409 Conflict if CRD already exists.
+ if (err.statusCode !== 409) throw err;
+ }
+ }
+
+ // Start pool operator's watcher loop.
+ //
+ // NOTE: Not getting the start sequence right can have catastrophic
+ // consequence leading to unintended pool destruction and data loss
+ // (i.e. when node info is available before the pool CRD is).
+ //
+ // The right order of steps is:
+ // 1. Get pool resources
+ // 2. Get info about pools on storage nodes
+ async start () {
+ var self = this;
+
+ // get pool k8s resources for initial synchronization and install
+ // event handlers to follow changes to them.
+ self._bindWatcher(self.watcher);
+ await self.watcher.start();
+
+ // this will start async processing of node and pool events
+ self.eventStream = new EventStream({ registry: self.registry });
+ self.eventStream.on('data', async (ev: any) => {
+ if (ev.kind === 'pool') {
+ await self.workq.push(ev, self._onPoolEvent.bind(self));
+ } else if (ev.kind === 'node' && (ev.eventType === 'sync' || ev.eventType === 'mod')) {
+ await self.workq.push(ev.object.name, self._onNodeSyncEvent.bind(self));
+ } else if (ev.kind === 'replica' && (ev.eventType === 'new' || ev.eventType === 'del')) {
+ await self.workq.push(ev, self._onReplicaEvent.bind(self));
+ }
+ });
+ }
+
+ // Handler for new/mod/del pool events
+ //
+ // @param ev Pool event as received from event stream.
+ //
+ async _onPoolEvent (ev: any) {
+ const name: string = ev.object.name;
+ const resource = this.watcher.get(name);
+
+ log.debug(`Received "${ev.eventType}" event for pool "${name}"`);
+
+ if (ev.eventType === 'new') {
+ if (resource === undefined) {
+ log.warn(`Unknown pool "${name}" will be destroyed`);
+ await this._destroyPool(name);
+ } else {
+ await this._updateResource(ev.object);
+ }
+ } else if (ev.eventType === 'mod') {
+ await this._updateResource(ev.object);
+ } else if (ev.eventType === 'del' && resource) {
+ log.warn(`Recreating destroyed pool "${name}"`);
+ await this._createPool(resource);
+ }
+ }
+
+ // Handler for node sync event.
+ //
+ // Either the node is new or came up after an outage - check that we
+ // don't have any pending pools waiting to be created on it.
+ //
+ // @param nodeName Name of the new node.
+ //
+ async _onNodeSyncEvent (nodeName: string) {
+ log.debug(`Syncing pool records for node "${nodeName}"`);
+
+ const resources = this.watcher.list().filter(
+ (ent) => ent.spec.node === nodeName
+ );
+ for (let i = 0; i < resources.length; i++) {
+ await this._createPool(resources[i]);
+ }
+ }
+
+ // Handler for new/del replica events
+ //
+ // @param ev Replica event as received from event stream.
+ //
+ async _onReplicaEvent (ev: any) {
+ const pool = ev.object.pool;
+ if (!pool) {
+ // can happen if the node goes away (replica will shortly disappear too)
+ return;
+ }
+ await this._updateFinalizer(pool.name, pool.replicas.length > 0);
+ }
+
+ // Stop the events, destroy event stream and reset resource cache.
+ stop () {
+ this.watcher.stop();
+ this.watcher.removeAllListeners();
+ if (this.eventStream) {
+ this.eventStream.destroy();
+ this.eventStream = null;
+ }
+ }
+
+ // Bind watcher's new/mod/del events to pool operator's callbacks.
+ //
+ // @param watcher k8s pool resource watcher.
+ //
+ _bindWatcher (watcher: CustomResourceCache) {
+ watcher.on('new', (resource: PoolResource) => {
+ this.workq.push(resource, this._createPool.bind(this));
+ });
+ watcher.on('mod', (resource: PoolResource) => {
+ this.workq.push(resource, this._modifyPool.bind(this));
+ });
+ watcher.on('del', (resource: PoolResource) => {
+ this.workq.push(resource, async (arg: PoolResource) => {
+ await this._destroyPool(arg.getName());
+ });
+ });
+ }
+
+ // Create a pool according to the specification.
+ // That includes parameters checks, node lookup and a call to registry
+ // to create the pool.
+ //
+ // @param resource Pool resource properties.
+ //
+ async _createPool (resource: PoolResource) {
+ const name: string = resource.getName();
+ const nodeName = resource.spec.node;
+
+ let pool = this.registry.getPool(name);
+ if (pool) {
+ // the pool already exists, just update its properties in k8s
+ await this._updateResource(pool);
+ return;
+ }
+
+ const node = this.registry.getNode(nodeName);
+ if (!node) {
+ const msg = `mayastor does not run on node "${nodeName}"`;
+ log.error(`Cannot create pool "${name}": ${msg}`);
+ await this._updateResourceProps(name, PoolState.Pending, msg);
+ return;
+ }
+ if (!node.isSynced()) {
+ const msg = `mayastor on node "${nodeName}" is offline`;
+ log.error(`Cannot sync pool "${name}": ${msg}`);
+ await this._updateResourceProps(name, PoolState.Pending, msg);
+ return;
+ }
+
+ // We will update the pool status once the pool is created, but
+ // that can take a time, so set reasonable default now.
+ await this._updateResourceProps(name, PoolState.Pending, 'Creating the pool');
+
+ try {
+ // pool resource props will be updated when "new" pool event is emitted
+ pool = await node.createPool(name, resource.spec.disks);
+ } catch (err) {
+ log.error(`Failed to create pool "${name}": ${err}`);
+ await this._updateResourceProps(name, PoolState.Error, err.toString());
+ }
+ }
+
+ // Remove the pool from internal state and if it exists destroy it.
+ // Does not throw - only logs an error.
+ //
+ // @param name Name of the pool to destroy.
+ //
+ async _destroyPool (name: string) {
+ var pool = this.registry.getPool(name);
+
+ if (pool) {
+ try {
+ await pool.destroy();
+ } catch (err) {
+ log.error(`Failed to destroy pool "${name}@${pool.node.name}": ${err}`);
+ }
+ }
+ }
+
+ // Changing pool parameters is actually not supported. However the pool
+ // operator's state should reflect the k8s state, so we make the change
+ // only at operator level and log a warning message.
+ //
+ // @param newPool New pool parameters.
+ //
+ async _modifyPool (resource: PoolResource) {
+ const name = resource.getName();
+ const pool = this.registry.getPool(name);
+ if (!pool) {
+ log.warn(`Ignoring modification to pool "${name}" that does not exist`);
+ return;
+ }
+ // Just now we don't even try to compare that the disks are the same as in
+ // the spec because mayastor returns disks prefixed by aio/iouring protocol
+ // and with uuid query parameter.
+ // TODO: Growing pools, mirrors, etc. is currently unsupported.
+
+ // Changing node implies destroying the pool on the old node and recreating
+ // it on the new node that is destructive action -> unsupported.
+ if (pool.node.name !== resource.spec.node) {
+ log.error(`Moving pool "${name}" between nodes is not supported`);
+ }
+ }
+
+ // Update status properties of k8s resource to be aligned with pool object
+ // properties.
+ //
+ // NOTE: This method does not throw if the update fails as there is nothing
+ // we can do if it fails. Though it logs an error message.
+ //
+ // @param pool Pool object.
+ //
+ async _updateResource (pool: any) {
+ var name = pool.name;
+ var resource = this.watcher.get(name);
+
+ // we don't track this pool so we cannot update the CRD
+ if (!resource) {
+ log.warn(`State of unknown pool "${name}" has changed`);
+ return;
+ }
+ var state = poolStateFromString(
+ pool.state.replace(/^POOL_/, '').toLowerCase()
+ );
+ var reason;
+ if (state === PoolState.Offline) {
+ reason = `mayastor does not run on the node "${pool.node}"`;
+ }
+
+ await this._updateResourceProps(
+ name,
+ state,
+ reason,
+ pool.disks,
+ pool.capacity,
+ pool.used,
+ );
+ }
+
+ // Update status properties of k8s CRD object.
+ //
+ // Parameters "name" and "state" are required, the rest is optional.
+ //
+ // NOTE: This method does not throw if the update fails as there is nothing
+ // we can do if it fails. Though we log an error message in such a case.
+ //
+ // @param name Name of the pool.
+ // @param state State of the pool.
+ // @param [reason] Reason describing the root cause of the state.
+ // @param [disks] Disk URIs.
+ // @param [capacity] Capacity of the pool in bytes.
+ // @param [used] Used bytes in the pool.
+ async _updateResourceProps (
+ name: string,
+ state: PoolState,
+ reason?: string,
+ disks?: string[],
+ capacity?: number,
+ used?: number,
+ ) {
+ try {
+ await this.watcher.updateStatus(name, (orig: PoolResource) => {
+ // avoid the update if the object has not changed
+ if (
+ state === orig.status.state &&
+ (reason === orig.status.reason || (!reason && !orig.status.reason)) &&
+ (capacity === undefined || capacity === orig.status.capacity) &&
+ (used === undefined || used === orig.status.used) &&
+ (disks === undefined || _.isEqual(disks, orig.status.disks))
+ ) {
+ return;
+ }
+
+ log.debug(`Updating properties of pool resource "${name}"`);
+ let resource: PoolResource = _.cloneDeep(orig);
+ resource.status = {
+ state: state,
+ reason: reason || ''
+ };
+ if (disks != null) {
+ resource.status.disks = disks;
+ }
+ if (capacity != null) {
+ resource.status.capacity = capacity;
+ }
+ if (used != null) {
+ resource.status.used = used;
+ }
+ return resource;
+ });
+ } catch (err) {
+ log.error(`Failed to update status of pool "${name}": ${err}`);
+ }
+ }
+
+ // Place or remove finalizer from pool resource.
+ //
+ // @param name Name of the pool.
+ // @param [busy] At least one replica on it.
+ async _updateFinalizer(name: string, busy: boolean) {
+ try {
+ if (busy) {
+ this.watcher.addFinalizer(name, POOL_FINALIZER);
+ } else {
+ this.watcher.removeFinalizer(name, POOL_FINALIZER);
+ }
+ } catch (err) {
+ log.error(`Failed to update finalizer on pool "${name}": ${err}`);
+ }
+ }
+}
diff --git a/csi/moac/test/index.js b/csi/moac/test/index.js
index 55489baa2..d80b36adb 100644
--- a/csi/moac/test/index.js
+++ b/csi/moac/test/index.js
@@ -24,7 +24,8 @@ const volumeOperator = require('./volume_operator_test.js');
const restApi = require('./rest_api_test.js');
const csiTest = require('./csi_test.js');
-logger.setLevel('debug');
+require('source-map-support').install();
+logger.setLevel('silly');
// Function form for terminating assertion properties to make JS linter happy
chai.use(dirtyChai);
diff --git a/csi/moac/test/nats_test.js b/csi/moac/test/nats_test.js
index 388bc5e0e..f9afd82f3 100644
--- a/csi/moac/test/nats_test.js
+++ b/csi/moac/test/nats_test.js
@@ -18,13 +18,13 @@ const RECONNECT_DELAY = 300;
const GRPC_ENDPOINT = '127.0.0.1:12345';
const NODE_NAME = 'node-name';
-var natsProc;
+let natsProc;
// Starts nats server and call callback when the server is up and ready.
function startNats (done) {
natsProc = spawn('nats-server', ['-a', NATS_HOST, '-p', NATS_PORT]);
- var doneCalled = false;
- var stderr = '';
+ let doneCalled = false;
+ let stderr = '';
natsProc.stderr.on('data', (data) => {
stderr += data.toString();
@@ -56,9 +56,25 @@ function stopNats () {
}
module.exports = function () {
- var eventBus;
- var registry;
- var nc;
+ let eventBus;
+ let registry;
+ let nc;
+ const sc = nats.StringCodec();
+
+ function connectNats (done) {
+ nats.connect({
+ servers: [`nats://${NATS_EP}`]
+ })
+ .then((res) => {
+ nc = res;
+ done();
+ })
+ .catch(() => {
+ setTimeout(() => {
+ connectNats(done);
+ }, 200);
+ });
+ }
// Create registry, event bus object, nats client and start nat server
before((done) => {
@@ -67,8 +83,7 @@ module.exports = function () {
eventBus = new MessageBus(registry, RECONNECT_DELAY);
startNats(err => {
if (err) return done(err);
- nc = nats.connect(`nats://${NATS_EP}`);
- nc.on('connect', () => done());
+ connectNats(done);
});
});
@@ -90,10 +105,10 @@ module.exports = function () {
});
it('should register a node', async () => {
- nc.publish('register', JSON.stringify({
- id: NODE_NAME,
- grpcEndpoint: GRPC_ENDPOINT
- }));
+ nc.publish('v0/registry', sc.encode(JSON.stringify({
+ id: 'v0/register',
+ data: { id: NODE_NAME, grpcEndpoint: GRPC_ENDPOINT }
+ })));
await waitUntil(async () => {
return registry.getNode(NODE_NAME);
}, 1000, 'new node');
@@ -103,31 +118,34 @@ module.exports = function () {
});
it('should ignore register request with missing node name', async () => {
- nc.publish('register', JSON.stringify({
- grpcEndpoint: GRPC_ENDPOINT
- }));
+ nc.publish('v0/registry', sc.encode(JSON.stringify({
+ id: 'v0/register',
+ data: { grpcEndpoint: GRPC_ENDPOINT }
+ })));
// small delay to wait for a possible crash of moac
await sleep(10);
});
it('should ignore register request with missing grpc endpoint', async () => {
- nc.publish('register', JSON.stringify({
- id: NODE_NAME
- }));
+ nc.publish('v0/registry', sc.encode(JSON.stringify({
+ id: 'v0/register',
+ data: { id: NODE_NAME }
+ })));
// small delay to wait for a possible crash of moac
await sleep(10);
});
it('should not crash upon a request with invalid JSON', async () => {
- nc.publish('register', '{"id": "NODE", "grpcEndpoint": "something"');
+ nc.publish('v0/register', sc.encode('{"id": "NODE", "grpcEndpoint": "something"'));
// small delay to wait for a possible crash of moac
await sleep(10);
});
it('should deregister a node', async () => {
- nc.publish('deregister', JSON.stringify({
- id: NODE_NAME
- }));
+ nc.publish('v0/registry', sc.encode(JSON.stringify({
+ id: 'v0/deregister',
+ data: { id: NODE_NAME }
+ })));
await waitUntil(async () => {
return !registry.getNode(NODE_NAME);
}, 1000, 'node removal');
diff --git a/csi/moac/test/node_operator_test.js b/csi/moac/test/node_operator_test.js
index ef273c7fd..c02b3eca6 100644
--- a/csi/moac/test/node_operator_test.js
+++ b/csi/moac/test/node_operator_test.js
@@ -1,26 +1,38 @@
// Unit tests for the node operator
-//
-// We don't test the init method which depends on k8s api client and watcher.
-// That method *must* be tested manually and in real k8s environment. For the
-// rest of the dependencies we provide fake objects which mimic the real
-// behaviour and allow us to test node operator in isolation from other
-// components.
'use strict';
const expect = require('chai').expect;
const sinon = require('sinon');
const sleep = require('sleep-promise');
+const { KubeConfig } = require('client-node-fixed-watcher');
const Registry = require('../registry');
-const NodeOperator = require('../node_operator');
+const { NodeOperator, NodeResource } = require('../node_operator');
+const { mockCache } = require('./watcher_stub');
const Node = require('./node_stub');
-const Watcher = require('./watcher_stub');
+const EVENT_PROPAGATION_DELAY = 10;
const NAME = 'node-name';
const NAMESPACE = 'mayastor';
const ENDPOINT = 'localhost:1234';
const ENDPOINT2 = 'localhost:1235';
+const fakeConfig = {
+ clusters: [
+ {
+ name: 'cluster',
+ server: 'foo.company.com'
+ }
+ ],
+ contexts: [
+ {
+ cluster: 'cluster',
+ user: 'user'
+ }
+ ],
+ users: [{ name: 'user' }]
+};
+
function defaultMeta (name) {
return {
creationTimestamp: '2019-02-15T18:23:53Z',
@@ -33,174 +45,151 @@ function defaultMeta (name) {
};
}
-module.exports = function () {
- var msStub, putStub, putStatusStub, deleteStub, postStub;
-
- // Create k8s node resource object
- function createNodeResource (name, grpcEndpoint, status) {
- const obj = {
- apiVersion: 'openebs.io/v1alpha1',
- kind: 'MayastorNode',
- metadata: defaultMeta(name),
- spec: { grpcEndpoint }
- };
- if (status) {
- obj.status = status;
- }
- return obj;
+// Create k8s node resource object
+function createK8sNodeResource (name, grpcEndpoint, status) {
+ const obj = {
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorNode',
+ metadata: defaultMeta(name),
+ spec: { grpcEndpoint }
+ };
+ if (status) {
+ obj.status = status;
}
+ return obj;
+}
- // k8s api client stub.
- //
- // Note that this stub serves only for PUT method on mayastor resource
- // endpoint to update the status of resource. Fake watcher that is used
- // in the tests does not use this client stub.
- function createK8sClient (watcher) {
- const mayastornodes = { mayastornodes: function (name) {} };
- const namespaces = function (ns) {
- expect(ns).to.equal(NAMESPACE);
- return mayastornodes;
- };
- const client = {
- apis: {
- 'openebs.io': {
- v1alpha1: { namespaces }
- }
- }
- };
-
- msStub = sinon.stub(mayastornodes, 'mayastornodes');
- msStub.post = async function (payload) {
- watcher.objects[payload.body.metadata.name] = payload.body;
- // simulate the asynchronicity of the put
- await sleep(1);
- };
- postStub = sinon.stub(msStub, 'post');
- postStub.callThrough();
-
- const msObject = {
- // the tricky thing here is that we have to update watcher's cache
- // if we use this fake k8s client to change the object in order to
- // mimic real behaviour.
- put: async function (payload) {
- watcher.objects[payload.body.metadata.name].spec = payload.body.spec;
- },
- delete: async function () {},
- status: {
- put: async function (payload) {
- watcher.objects[payload.body.metadata.name].status =
- payload.body.status;
- }
- }
- };
- putStub = sinon.stub(msObject, 'put');
- putStub.callThrough();
- putStatusStub = sinon.stub(msObject.status, 'put');
- putStatusStub.callThrough();
- deleteStub = sinon.stub(msObject, 'delete');
- deleteStub.callThrough();
- msStub.returns(msObject);
- return client;
- }
+// Create k8s node resource object
+function createNodeResource (name, grpcEndpoint, status) {
+ return new NodeResource(createK8sNodeResource(name, grpcEndpoint, status));
+}
- // Create a pool operator object suitable for testing - with fake watcher
- // and fake k8s api client.
- async function mockedNodeOperator (k8sObjects, registry) {
- const oper = new NodeOperator(NAMESPACE);
- oper.registry = registry;
- oper.watcher = new Watcher(oper._filterMayastorNode, k8sObjects);
- oper.k8sClient = createK8sClient(oper.watcher);
-
- await oper.start();
- // give event-stream time to run its _start method to prevent race
- // conditions in test code when the underlaying source is modified
- // before _start is run.
- await sleep(1);
- return oper;
- }
+// Create a pool operator object suitable for testing - with fake watcher
+// and fake k8s api client.
+function createNodeOperator (registry) {
+ const kc = new KubeConfig();
+ Object.assign(kc, fakeConfig);
+ return new NodeOperator(NAMESPACE, kc, registry);
+}
- describe('resource filter', () => {
- it('valid mayastor node with status should pass the filter', () => {
+module.exports = function () {
+ describe('NodeResource constructor', () => {
+ it('should create valid node resource with status', () => {
const obj = createNodeResource(NAME, ENDPOINT, 'online');
- const res = NodeOperator.prototype._filterMayastorNode(obj);
- expect(res.metadata.name).to.equal(NAME);
- expect(res.spec.grpcEndpoint).to.equal(ENDPOINT);
- expect(res.status).to.equal('online');
+ expect(obj.metadata.name).to.equal(NAME);
+ expect(obj.spec.grpcEndpoint).to.equal(ENDPOINT);
+ expect(obj.status).to.equal('online');
});
- it('valid mayastor node without status should pass the filter', () => {
+ it('should create valid node resource without status', () => {
const obj = createNodeResource(NAME, ENDPOINT);
- const res = NodeOperator.prototype._filterMayastorNode(obj);
- expect(res.metadata.name).to.equal(NAME);
- expect(res.spec.grpcEndpoint).to.equal(ENDPOINT);
- expect(res.status).to.equal('unknown');
+ expect(obj.metadata.name).to.equal(NAME);
+ expect(obj.spec.grpcEndpoint).to.equal(ENDPOINT);
+ expect(obj.status).to.equal('unknown');
});
- it('mayastor node without grpc-endpoint should be ignored', () => {
- const obj = createNodeResource(NAME);
- const res = NodeOperator.prototype._filterMayastorNode(obj);
- expect(res).to.be.null();
+ it('should not create node resource without grpc endpoint', () => {
+ expect(() => createNodeResource(NAME)).to.throw();
});
});
- describe('watcher events', () => {
- var oper; // node operator
+ describe('init method', () => {
+ let kc, oper, fakeApiStub;
- afterEach(async () => {
+ beforeEach(() => {
+ const registry = new Registry();
+ kc = new KubeConfig();
+ Object.assign(kc, fakeConfig);
+ oper = new NodeOperator(NAMESPACE, kc, registry);
+ const makeApiStub = sinon.stub(kc, 'makeApiClient');
+ const fakeApi = {
+ createCustomResourceDefinition: () => null
+ };
+ fakeApiStub = sinon.stub(fakeApi, 'createCustomResourceDefinition');
+ makeApiStub.returns(fakeApi);
+ });
+
+ afterEach(() => {
if (oper) {
- await oper.stop();
- oper = null;
+ oper.stop();
+ oper = undefined;
}
});
- it('should add node to registry for existing resource when starting the operator', async () => {
- const registry = new Registry();
+ it('should create CRD if it does not exist', async () => {
+ fakeApiStub.resolves();
+ await oper.init(kc);
+ });
+
+ it('should ignore error if CRD already exists', async () => {
+ fakeApiStub.rejects({
+ statusCode: 409
+ });
+ await oper.init(kc);
+ });
+
+ it('should throw if CRD creation fails', async () => {
+ fakeApiStub.rejects({
+ statusCode: 404
+ });
+ try {
+ await oper.init(kc);
+ } catch (err) {
+ return;
+ }
+ throw new Error('Init did not fail');
+ });
+ });
+
+ describe('watcher events', () => {
+ let oper; // node operator
+ let stubs, registry, nodeResource;
+
+ beforeEach(async () => {
+ registry = new Registry();
registry.Node = Node;
- const addNodeSpy = sinon.spy(registry, 'addNode');
- oper = await mockedNodeOperator(
- [createNodeResource(NAME, ENDPOINT, 'online')],
- registry
- );
- sinon.assert.calledOnce(addNodeSpy);
- sinon.assert.calledWith(addNodeSpy, NAME, ENDPOINT);
+ oper = createNodeOperator(registry);
+ nodeResource = createNodeResource(NAME, ENDPOINT, 'online');
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(nodeResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ });
+
+ afterEach(() => {
+ if (oper) {
+ oper.stop();
+ oper = null;
+ }
});
it('should add node to registry upon "new" event', async () => {
- const registry = new Registry();
- registry.Node = Node;
const addNodeSpy = sinon.spy(registry, 'addNode');
- oper = await mockedNodeOperator([], registry);
- // trigger "new" event
- oper.watcher.newObject(createNodeResource(NAME, ENDPOINT));
+ oper.watcher.emit('new', nodeResource);
+ await sleep(EVENT_PROPAGATION_DELAY);
sinon.assert.calledOnce(addNodeSpy);
sinon.assert.calledWith(addNodeSpy, NAME, ENDPOINT);
});
it('should remove node from registry upon "del" event', async () => {
// create registry with a node
- const registry = new Registry();
const node = new Node(NAME);
node.connect(ENDPOINT);
registry.nodes[NAME] = node;
- const addNodeSpy = sinon.spy(registry, 'addNode');
const removeNodeSpy = sinon.spy(registry, 'removeNode');
- oper = await mockedNodeOperator(
- [createNodeResource(NAME, ENDPOINT, 'online')],
- registry
- );
- sinon.assert.calledOnce(addNodeSpy);
// trigger "del" event
- oper.watcher.delObject(NAME);
- sinon.assert.calledOnce(addNodeSpy);
- sinon.assert.calledOnce(removeNodeSpy);
+ oper.watcher.emit('del', nodeResource);
+ await sleep(EVENT_PROPAGATION_DELAY);
sinon.assert.calledWith(removeNodeSpy, NAME);
});
it('should not do anything upon "mod" event', async () => {
// create registry with a node
- const registry = new Registry();
const node = new Node(NAME);
node.connect(ENDPOINT);
registry.nodes[NAME] = node;
@@ -209,242 +198,270 @@ module.exports = function () {
const removeNodeStub = sinon.stub(registry, 'removeNode');
removeNodeStub.returns();
- oper = await mockedNodeOperator(
- [createNodeResource(NAME, ENDPOINT, 'online')],
- registry
- );
- sinon.assert.calledOnce(addNodeStub);
// trigger "mod" event
- oper.watcher.modObject(createNodeResource(NAME, ENDPOINT));
+ oper.watcher.emit('mod', nodeResource);
+ await sleep(EVENT_PROPAGATION_DELAY);
sinon.assert.notCalled(removeNodeStub);
- sinon.assert.calledOnce(addNodeStub);
+ sinon.assert.notCalled(addNodeStub);
});
});
- describe('registry node events', () => {
- var oper; // node operator
+ describe('registry events', () => {
+ let registry, oper;
+
+ beforeEach(async () => {
+ registry = new Registry();
+ registry.Node = Node;
+ oper = createNodeOperator(registry);
+ });
- afterEach(async () => {
+ afterEach(() => {
if (oper) {
- await oper.stop();
+ oper.stop();
oper = null;
}
});
it('should create a resource upon "new" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator([], registry);
- registry.addNode(NAME, ENDPOINT);
- await sleep(20);
- sinon.assert.calledOnce(postStub);
- sinon.assert.calledWithMatch(postStub, {
- body: {
- metadata: {
- name: NAME,
- namespace: NAMESPACE
- },
- spec: {
- grpcEndpoint: ENDPOINT
- }
- }
- });
- sinon.assert.notCalled(putStub);
- sinon.assert.calledOnce(putStatusStub);
- sinon.assert.calledWithMatch(putStatusStub, {
- body: {
- status: 'online'
- }
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.onFirstCall().returns();
+ stubs.get.onSecondCall().returns(nodeResource);
});
- sinon.assert.notCalled(deleteStub);
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ registry.addNode(NAME, ENDPOINT);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.calledOnce(stubs.create);
+ expect(stubs.create.args[0][4].metadata.name).to.equal(NAME);
+ expect(stubs.create.args[0][4].spec.grpcEndpoint).to.equal(ENDPOINT);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.delete);
});
it('should not crash if POST fails upon "new" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator([], registry);
- postStub.rejects(new Error('post failed'));
- registry.addNode(NAME, ENDPOINT);
- await sleep(10);
- sinon.assert.calledOnce(postStub);
- sinon.assert.calledWithMatch(postStub, {
- body: {
- metadata: {
- name: NAME,
- namespace: NAMESPACE
- },
- spec: {
- grpcEndpoint: ENDPOINT
- }
- }
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.onFirstCall().returns();
+ stubs.get.onSecondCall().returns(nodeResource);
+ stubs.create.rejects(new Error('post failed'));
});
- sinon.assert.notCalled(putStatusStub);
- sinon.assert.notCalled(deleteStub);
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ registry.addNode(NAME, ENDPOINT);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.calledOnce(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.delete);
});
it('should update the resource upon "new" node event if it exists', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator([], registry);
- oper.watcher.injectObject(createNodeResource(NAME, ENDPOINT, 'offline'));
- registry.addNode(NAME, ENDPOINT2);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- spec: {
- grpcEndpoint: ENDPOINT2
- }
- }
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT, 'offline');
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(nodeResource);
});
- sinon.assert.calledOnce(putStatusStub);
- sinon.assert.calledWithMatch(putStatusStub, {
- body: {
- status: 'online'
- }
- });
- sinon.assert.notCalled(deleteStub);
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ registry.addNode(NAME, ENDPOINT2);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.calledOnce(stubs.update);
+ expect(stubs.update.args[0][5].metadata.name).to.equal(NAME);
+ expect(stubs.update.args[0][5].spec.grpcEndpoint).to.equal(ENDPOINT2);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].metadata.name).to.equal(NAME);
+ expect(stubs.updateStatus.args[0][5].status).to.equal('online');
+ sinon.assert.notCalled(stubs.delete);
});
it('should not update the resource upon "new" node event if it is the same', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator([], registry);
- oper.watcher.injectObject(createNodeResource(NAME, ENDPOINT, 'online'));
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT, 'online');
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(nodeResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
registry.addNode(NAME, ENDPOINT);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.notCalled(putStatusStub);
- sinon.assert.notCalled(deleteStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.delete);
});
it('should update the resource upon "mod" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator(
- [createNodeResource(NAME, ENDPOINT, 'online')],
- registry
- );
- registry.addNode(NAME, ENDPOINT2);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- spec: {
- grpcEndpoint: ENDPOINT2
- }
- }
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT, 'online');
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(nodeResource);
});
- sinon.assert.notCalled(putStatusStub);
- sinon.assert.notCalled(deleteStub);
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ registry.addNode(NAME, ENDPOINT2);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.calledOnce(stubs.update);
+ expect(stubs.update.args[0][5].metadata.name).to.equal(NAME);
+ expect(stubs.update.args[0][5].spec.grpcEndpoint).to.equal(ENDPOINT2);
+ sinon.assert.notCalled(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.delete);
});
it('should update status of the resource upon "mod" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator(
- [createNodeResource(NAME, ENDPOINT, 'online')],
- registry
- );
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT, 'online');
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(nodeResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
registry.addNode(NAME, ENDPOINT);
+ await sleep(EVENT_PROPAGATION_DELAY);
const node = registry.getNode(NAME);
const isSyncedStub = sinon.stub(node, 'isSynced');
isSyncedStub.returns(false);
node._offline();
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.calledOnce(putStatusStub);
- sinon.assert.calledWithMatch(putStatusStub, {
- body: {
- status: 'offline'
- }
- });
- sinon.assert.notCalled(deleteStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].metadata.name).to.equal(NAME);
+ expect(stubs.updateStatus.args[0][5].status).to.equal('offline');
+ sinon.assert.notCalled(stubs.delete);
});
it('should not crash if PUT fails upon "mod" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator(
- [createNodeResource(NAME, ENDPOINT, 'online')],
- registry
- );
- putStub.rejects(new Error('put failed'));
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT, 'online');
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(nodeResource);
+ stubs.update.rejects(new Error('put failed'));
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
registry.addNode(NAME, ENDPOINT2);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.calledOnce(putStub);
- sinon.assert.notCalled(putStatusStub);
- sinon.assert.notCalled(deleteStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.calledTwice(stubs.update);
+ sinon.assert.notCalled(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.delete);
});
- it('should not crash if the resource does not exist upon "mod" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator([], registry);
+ it('should not create the resource upon "mod" node event', async () => {
+ let stubs;
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns();
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// secretly inject node to registry (watcher does not know)
const node = new Node(NAME);
node.connect(ENDPOINT);
registry.nodes[NAME] = node;
- // modify the node
registry.addNode(NAME, ENDPOINT2);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.notCalled(putStatusStub);
- sinon.assert.notCalled(deleteStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.delete);
});
it('should delete the resource upon "del" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator(
- [createNodeResource(NAME, ENDPOINT, 'online')],
- registry
- );
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT, 'online');
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(nodeResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ // secretly inject node to registry (watcher does not know)
+ const node = new Node(NAME);
+ node.connect(ENDPOINT);
+ registry.nodes[NAME] = node;
registry.removeNode(NAME);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.notCalled(putStatusStub);
- sinon.assert.calledOnce(deleteStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.updateStatus);
+ sinon.assert.calledOnce(stubs.delete);
});
it('should not crash if DELETE fails upon "del" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator(
- [createNodeResource(NAME, ENDPOINT, 'online')],
- registry
- );
- deleteStub.rejects(new Error('delete failed'));
+ let stubs;
+ const nodeResource = createNodeResource(NAME, ENDPOINT, 'online');
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(nodeResource);
+ stubs.delete.rejects(new Error('delete failed'));
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ // secretly inject node to registry (watcher does not know)
+ const node = new Node(NAME);
+ node.connect(ENDPOINT);
+ registry.nodes[NAME] = node;
registry.removeNode(NAME);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.notCalled(putStatusStub);
- sinon.assert.calledOnce(deleteStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.updateStatus);
+ sinon.assert.calledOnce(stubs.delete);
});
it('should not crash if the resource does not exist upon "del" node event', async () => {
- const registry = new Registry();
- registry.Node = Node;
- oper = await mockedNodeOperator([], registry);
+ let stubs;
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// secretly inject node to registry (watcher does not know)
const node = new Node(NAME);
node.connect(ENDPOINT);
registry.nodes[NAME] = node;
- // modify the node
registry.removeNode(NAME);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.notCalled(putStatusStub);
- sinon.assert.notCalled(deleteStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.delete);
});
});
};
diff --git a/csi/moac/test/pool_operator_test.js b/csi/moac/test/pool_operator_test.js
index 6317d190a..8fab1fa45 100644
--- a/csi/moac/test/pool_operator_test.js
+++ b/csi/moac/test/pool_operator_test.js
@@ -1,17 +1,10 @@
// Unit tests for the pool operator
//
-// We don't test the init method which depends on k8s api client and watcher.
-// That method *must* be tested manually and in real k8s environment. For the
-// rest of the dependencies we provide fake objects which mimic the real
-// behaviour and allow us to test pool operator in isolation from other
-// components.
-//
// Pool operator depends on a couple of modules:
// * registry (real)
// * node object (fake)
// * pool object (fake)
-// * watcher (fake)
-// * k8s client (fake)
+// * watcher (mocked)
//
// As you can see most of them must be fake in order to do detailed testing
// of pool operator. That makes the code more complicated and less readable.
@@ -21,162 +14,232 @@
const expect = require('chai').expect;
const sinon = require('sinon');
const sleep = require('sleep-promise');
+const { KubeConfig } = require('client-node-fixed-watcher');
const Registry = require('../registry');
const { GrpcError, GrpcCode } = require('../grpc_client');
-const PoolOperator = require('../pool_operator');
+const { PoolOperator, PoolResource } = require('../pool_operator');
const { Pool } = require('../pool');
-const Watcher = require('./watcher_stub');
+const { Replica } = require('../replica');
+const { mockCache } = require('./watcher_stub');
const Node = require('./node_stub');
const NAMESPACE = 'mayastor';
+const EVENT_PROPAGATION_DELAY = 10;
-module.exports = function () {
- var msStub, putStub;
+const fakeConfig = {
+ clusters: [
+ {
+ name: 'cluster',
+ server: 'foo.company.com'
+ }
+ ],
+ contexts: [
+ {
+ cluster: 'cluster',
+ user: 'user'
+ }
+ ],
+ users: [{ name: 'user' }]
+};
- // Create k8s pool resource object
- function createPoolResource (
+// Create k8s pool resource object
+function createK8sPoolResource (
+ name,
+ node,
+ disks,
+ finalizers,
+ state,
+ reason,
+ capacity,
+ used
+) {
+ const obj = {
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorPool',
+ metadata: {
+ creationTimestamp: '2019-02-15T18:23:53Z',
+ generation: 1,
+ name: name,
+ namespace: NAMESPACE,
+ finalizers: finalizers,
+ resourceVersion: '627981',
+ selfLink: `/apis/openebs.io/v1alpha1/namespaces/${NAMESPACE}/mayastorpools/${name}`,
+ uid: 'd99f06a9-314e-11e9-b086-589cfc0d76a7'
+ },
+ spec: {
+ node: node,
+ disks: disks
+ }
+ };
+ if (state) {
+ const status = { state };
+ status.disks = disks.map((d) => `aio://${d}`);
+ if (reason != null) status.reason = reason;
+ if (capacity != null) status.capacity = capacity;
+ if (used != null) status.used = used;
+ obj.status = status;
+ }
+ return obj;
+}
+
+function createPoolResource (
+ name,
+ node,
+ disks,
+ finalizers,
+ state,
+ reason,
+ capacity,
+ used
+) {
+ return new PoolResource(createK8sPoolResource(
name,
node,
disks,
+ finalizers,
state,
reason,
capacity,
used
- ) {
- const obj = {
- apiVersion: 'openebs.io/v1alpha1',
- kind: 'MayastorPool',
- metadata: {
- creationTimestamp: '2019-02-15T18:23:53Z',
- generation: 1,
- name: name,
- namespace: NAMESPACE,
- resourceVersion: '627981',
- selfLink: `/apis/openebs.io/v1alpha1/namespaces/${NAMESPACE}/mayastorpools/${name}`,
- uid: 'd99f06a9-314e-11e9-b086-589cfc0d76a7'
- },
- spec: {
- node: node,
- disks: disks
- }
- };
- if (state) {
- const status = { state };
- status.disks = disks.map((d) => `aio://${d}`);
- if (reason != null) status.reason = reason;
- if (capacity != null) status.capacity = capacity;
- if (used != null) status.used = used;
- obj.status = status;
- }
- return obj;
- }
-
- // k8s api client stub.
- //
- // Note that this stub serves only for PUT method on mayastor resource
- // endpoint to update the status of resource. Fake watcher that is used
- // in the tests does not use this client stub.
- function createK8sClient (watcher) {
- const mayastorpools = { mayastorpools: function (name) {} };
- const namespaces = function (ns) {
- expect(ns).to.equal(NAMESPACE);
- return mayastorpools;
- };
- const client = {
- apis: {
- 'openebs.io': {
- v1alpha1: { namespaces }
- }
- }
- };
- msStub = sinon.stub(mayastorpools, 'mayastorpools');
- const msObject = {
- status: {
- // the tricky thing here is that we have to update watcher's cache
- // if we use this fake k8s client to change the object in order to
- // mimic real behaviour.
- put: async function (payload) {
- watcher.objects[payload.body.metadata.name].status =
- payload.body.status;
- // simulate the asynchronicity of the put
- // await sleep(1);
- }
- }
- };
- putStub = sinon.stub(msObject.status, 'put');
- putStub.callThrough();
- msStub.returns(msObject);
- return client;
- }
-
- // Create a pool operator object suitable for testing - with fake watcher
- // and fake k8s api client.
- async function MockedPoolOperator (k8sObjects, nodes) {
- const oper = new PoolOperator(NAMESPACE);
- const registry = new Registry();
- registry.Node = Node;
- nodes = nodes || [];
- nodes.forEach((n) => (registry.nodes[n.name] = n));
- oper.registry = registry;
- oper.watcher = new Watcher(oper._filterMayastorPool, k8sObjects);
- oper.k8sClient = createK8sClient(oper.watcher);
-
- await oper.start();
-
- // Let the initial "new" events pass by so that they don't interfere with
- // whatever we are going to do with the operator after we return.
- //
- // TODO: Hardcoded delays are ugly. Find a better way. Applies to all
- // sleeps in this file.
- if (nodes.length > 0) {
- await sleep(10);
- }
+ ));
+}
+
+// Create a pool operator object suitable for testing - with mocked watcher etc.
+function createPoolOperator (nodes) {
+ const registry = new Registry();
+ registry.Node = Node;
+ nodes = nodes || [];
+ nodes.forEach((n) => (registry.nodes[n.name] = n));
+ const kc = new KubeConfig();
+ Object.assign(kc, fakeConfig);
+ return new PoolOperator(NAMESPACE, kc, registry);
+}
- return oper;
- }
-
- describe('resource filter', () => {
- it('valid mayastor pool should pass the filter', () => {
+module.exports = function () {
+ describe('PoolResource constructor', () => {
+ it('should create valid mayastor pool with status', () => {
const obj = createPoolResource(
'pool',
'node',
['/dev/sdc', '/dev/sdb'],
- 'OFFLINE',
+ ['some.finalizer.com'],
+ 'offline',
'The node is down'
);
- const res = PoolOperator.prototype._filterMayastorPool(obj);
- expect(res).to.have.all.keys('name', 'node', 'disks');
- expect(res.name).to.equal('pool');
- expect(res.node).to.equal('node');
+ expect(obj.metadata.name).to.equal('pool');
+ expect(obj.spec.node).to.equal('node');
// the filter should sort the disks
- expect(JSON.stringify(res.disks)).to.equal(
+ expect(JSON.stringify(obj.spec.disks)).to.equal(
JSON.stringify(['/dev/sdb', '/dev/sdc'])
);
- expect(res.state).to.be.undefined();
+ expect(obj.status.state).to.equal('offline');
+ expect(obj.status.reason).to.equal('The node is down');
+ expect(obj.status.disks).to.deep.equal(['aio:///dev/sdc', 'aio:///dev/sdb']);
+ expect(obj.status.capacity).to.be.undefined();
+ expect(obj.status.used).to.be.undefined();
});
- it('valid mayastor pool without status should pass the filter', () => {
+ it('should create valid mayastor pool without status', () => {
const obj = createPoolResource('pool', 'node', ['/dev/sdc', '/dev/sdb']);
- const res = PoolOperator.prototype._filterMayastorPool(obj);
- expect(res).to.have.all.keys('name', 'node', 'disks');
- expect(res.name).to.equal('pool');
- expect(res.node).to.equal('node');
- expect(res.state).to.be.undefined();
+ expect(obj.metadata.name).to.equal('pool');
+ expect(obj.spec.node).to.equal('node');
+ expect(obj.status.state).to.equal('unknown');
+ });
+
+ it('should not create mayastor pool without node specification', () => {
+ expect(() => createPoolResource(
+ 'pool',
+ undefined,
+ ['/dev/sdc', '/dev/sdb']
+ )).to.throw();
+ });
+ });
+
+ describe('init method', () => {
+ let kc, oper, fakeApiStub;
+
+ beforeEach(() => {
+ const registry = new Registry();
+ kc = new KubeConfig();
+ Object.assign(kc, fakeConfig);
+ oper = new PoolOperator(NAMESPACE, kc, registry);
+ const makeApiStub = sinon.stub(kc, 'makeApiClient');
+ const fakeApi = {
+ createCustomResourceDefinition: () => null
+ };
+ fakeApiStub = sinon.stub(fakeApi, 'createCustomResourceDefinition');
+ makeApiStub.returns(fakeApi);
+ });
+
+ afterEach(() => {
+ if (oper) {
+ oper.stop();
+ oper = undefined;
+ }
+ });
+
+ it('should create CRD if it does not exist', async () => {
+ fakeApiStub.resolves();
+ await oper.init(kc);
+ });
+
+ it('should ignore error if CRD already exists', async () => {
+ fakeApiStub.rejects({
+ statusCode: 409
+ });
+ await oper.init(kc);
+ });
+
+ it('should throw if CRD creation fails', async () => {
+ fakeApiStub.rejects({
+ statusCode: 404
+ });
+ try {
+ await oper.init(kc);
+ } catch (err) {
+ return;
+ }
+ throw new Error('Init did not fail');
});
});
describe('watcher events', () => {
- var oper; // pool operator
+ let oper; // pool operator
- afterEach(async () => {
+ afterEach(() => {
if (oper) {
- await oper.stop();
+ oper.stop();
oper = null;
}
});
describe('new event', () => {
+ it('should process resources that existed before the operator was started', async () => {
+ let stubs;
+ oper = createPoolOperator([]);
+ const poolResource = createPoolResource('pool', 'node', ['/dev/sdb']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ stubs.list.returns([poolResource]);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].metadata.name).to.equal('pool');
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'mayastor does not run on node "node"'
+ });
+ });
+
it('should set "state" to PENDING when creating a pool', async () => {
+ let stubs;
const node = new Node('node');
const createPoolStub = sinon.stub(node, 'createPool');
createPoolStub.resolves(
@@ -189,37 +252,35 @@ module.exports = function () {
used: 10
})
);
- oper = await MockedPoolOperator([], [node]);
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource('pool', 'node', ['/dev/sdb']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "new" event
- oper.watcher.newObject(
- createPoolResource('pool', 'node', ['/dev/sdb'])
- );
-
+ oper.watcher.emit('new', poolResource);
// give event callbacks time to propagate
- await sleep(10);
+ await sleep(EVENT_PROPAGATION_DELAY);
sinon.assert.calledOnce(createPoolStub);
sinon.assert.calledWith(createPoolStub, 'pool', ['/dev/sdb']);
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- kind: 'MayastorPool',
- metadata: {
- name: 'pool',
- generation: 1,
- resourceVersion: '627981'
- },
- status: {
- state: 'pending',
- reason: 'Creating the pool'
- }
- }
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].metadata.name).to.equal('pool');
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'Creating the pool'
});
});
it('should not try to create a pool if the node has not been synced', async () => {
+ let stubs;
const node = new Node('node');
sinon.stub(node, 'isSynced').returns(false);
const createPoolStub = sinon.stub(node, 'createPool');
@@ -233,21 +294,30 @@ module.exports = function () {
used: 10
})
);
- oper = await MockedPoolOperator([], [node]);
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource('pool', 'node', ['/dev/sdb']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "new" event
- oper.watcher.newObject(
- createPoolResource('pool', 'node', ['/dev/sdb'])
- );
-
+ oper.watcher.emit('new', poolResource);
// give event callbacks time to propagate
- await sleep(10);
+ await sleep(EVENT_PROPAGATION_DELAY);
sinon.assert.notCalled(createPoolStub);
- sinon.assert.notCalled(msStub);
- sinon.assert.notCalled(putStub);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
});
it('should not try to create a pool when pool with the same name already exists', async () => {
+ let stubs;
+ const node = new Node('node', {}, []);
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
@@ -255,270 +325,248 @@ module.exports = function () {
capacity: 100,
used: 10
});
- const node = new Node('node', {}, []);
const createPoolStub = sinon.stub(node, 'createPool');
createPoolStub.resolves(pool);
- oper = await MockedPoolOperator([], [node]);
+
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource('pool', 'node', ['/dev/sdb', '/dev/sdc']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// this creates the inconsistency between real and k8s state which we are testing
node.pools.push(pool);
// trigger "new" event
- oper.watcher.newObject(
- // does not matter that the disks are different - still the same pool
- createPoolResource('pool', 'node', ['/dev/sdb', '/dev/sdc'])
- );
-
+ oper.watcher.emit('new', poolResource);
// give event callbacks time to propagate
- await sleep(10);
-
- // the stub is called when the new node is synced
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'degraded',
- reason: '',
- disks: ['aio:///dev/sdb'],
- capacity: 100,
- used: 10
- }
- }
- });
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.notCalled(createPoolStub);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'degraded',
+ reason: '',
+ disks: ['aio:///dev/sdb'],
+ capacity: 100,
+ used: 10
+ });
});
// important test as moving the pool between nodes would destroy data
it('should leave the pool untouched when pool exists and is on a different node', async () => {
+ let stubs;
+ const node1 = new Node('node1', {}, []);
+ const node2 = new Node('node2');
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
- state: 'POOL_ONLINE',
+ state: 'POOL_DEGRADED',
capacity: 100,
used: 10
});
- const node1 = new Node('node1', {}, []);
- const node2 = new Node('node2');
const createPoolStub1 = sinon.stub(node1, 'createPool');
const createPoolStub2 = sinon.stub(node2, 'createPool');
createPoolStub1.resolves(pool);
createPoolStub2.resolves(pool);
- oper = await MockedPoolOperator([], [node1, node2]);
+
+ oper = createPoolOperator([node1, node2]);
+ const poolResource = createPoolResource('pool', 'node2', ['/dev/sdb', '/dev/sdc']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// we assign the pool to node1 but later in the event it will be on node2
node1.pools.push(pool);
// trigger "new" event
- oper.watcher.newObject(
- // does not matter that the disks are different - still the same pool
- createPoolResource('pool', 'node2', ['/dev/sdb', '/dev/sdc'])
- );
-
+ oper.watcher.emit('new', poolResource);
// give event callbacks time to propagate
- await sleep(10);
-
- // the stub is called when the new node is synced
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'online',
- reason: '',
- disks: ['aio:///dev/sdb']
- }
- }
- });
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.notCalled(createPoolStub1);
sinon.assert.notCalled(createPoolStub2);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'degraded',
+ reason: '',
+ disks: ['aio:///dev/sdb'],
+ capacity: 100,
+ used: 10
+ });
});
it('should set "reason" to error message when create pool fails', async () => {
+ let stubs;
const node = new Node('node');
const createPoolStub = sinon.stub(node, 'createPool');
createPoolStub.rejects(
new GrpcError(GrpcCode.INTERNAL, 'create failed')
);
- oper = await MockedPoolOperator([], [node]);
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource('pool', 'node', ['/dev/sdb']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "new" event
- oper.watcher.newObject(
- createPoolResource('pool', 'node', ['/dev/sdb'])
- );
-
+ oper.watcher.emit('new', poolResource);
// give event callbacks time to propagate
- await sleep(10);
-
- sinon.assert.calledTwice(msStub);
- sinon.assert.alwaysCalledWith(msStub, 'pool');
- sinon.assert.calledTwice(putStub);
- sinon.assert.calledWithMatch(putStub.firstCall, {
- body: {
- status: {
- state: 'pending',
- reason: 'Creating the pool'
- }
- }
- });
- sinon.assert.calledWithMatch(putStub.secondCall, {
- body: {
- status: {
- state: 'pending',
- reason: 'Error: create failed'
- }
- }
- });
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.calledOnce(createPoolStub);
sinon.assert.calledWith(createPoolStub, 'pool', ['/dev/sdb']);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledTwice(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'Creating the pool'
+ });
+ expect(stubs.updateStatus.args[1][5].status).to.deep.equal({
+ state: 'error',
+ reason: 'Error: create failed'
+ });
});
it('should ignore failure to update the resource state', async () => {
+ let stubs;
const node = new Node('node');
const createPoolStub = sinon.stub(node, 'createPool');
createPoolStub.rejects(
new GrpcError(GrpcCode.INTERNAL, 'create failed')
);
- oper = await MockedPoolOperator([], [node]);
- putStub.rejects(new Error('http put error'));
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource('pool', 'node', ['/dev/sdb']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ stubs.updateStatus.resolves(new Error('http put error'));
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "new" event
- oper.watcher.newObject(
- createPoolResource('pool', 'node', ['/dev/sdb'])
- );
-
+ oper.watcher.emit('new', poolResource);
// give event callbacks time to propagate
- await sleep(10);
-
- sinon.assert.calledTwice(msStub);
- sinon.assert.alwaysCalledWith(msStub, 'pool');
- sinon.assert.calledTwice(putStub);
- sinon.assert.calledWithMatch(putStub.firstCall, {
- body: {
- status: {
- state: 'pending',
- reason: 'Creating the pool'
- }
- }
- });
- sinon.assert.calledWithMatch(putStub.secondCall, {
- body: {
- status: {
- state: 'pending',
- reason: 'Error: create failed'
- }
- }
- });
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.calledOnce(createPoolStub);
sinon.assert.calledWith(createPoolStub, 'pool', ['/dev/sdb']);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledTwice(stubs.updateStatus);
});
it('should not create a pool if node does not exist', async () => {
- oper = await MockedPoolOperator([], []);
- // trigger "new" event
- oper.watcher.newObject(
- createPoolResource('pool', 'node', ['/dev/sdb'])
- );
-
- // give event callbacks time to propagate
- await sleep(10);
-
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'pending',
- reason: 'mayastor does not run on node "node"'
- }
- }
+ let stubs;
+ oper = createPoolOperator([]);
+ const poolResource = createPoolResource('pool', 'node', ['/dev/sdb']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
});
- });
-
- it('when a pool is pre-imported it should be created once the node arrives and is synced', async () => {
- const node = new Node('node');
- oper = await MockedPoolOperator([createPoolResource('pool', 'node', ['/dev/sdb'])], [node]);
-
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ // trigger "new" event
+ oper.watcher.emit('new', poolResource);
// give event callbacks time to propagate
- await sleep(10);
-
- sinon.assert.calledTwice(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledTwice(putStub);
-
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'pending',
- reason: 'Error: Broken connection to mayastor on node "node"'
- }
- }
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'mayastor does not run on node "node"'
});
});
it('should create a pool once the node arrives and is synced', async () => {
- oper = await MockedPoolOperator([], []);
- oper.watcher.newObject(
- createPoolResource('pool', 'node', ['/dev/sdb'])
- );
-
- // give event callbacks time to propagate
- await sleep(10);
-
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'pending',
- reason: 'mayastor does not run on node "node"'
- }
- }
+ let stubs;
+ oper = createPoolOperator([]);
+ const poolResource = createPoolResource('pool', 'node', ['/dev/sdb']);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ stubs.list.returns([poolResource]);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'mayastor does not run on node "node"'
});
const node = new Node('node');
+ const syncedStub = sinon.stub(node, 'isSynced');
+ syncedStub.returns(false);
oper.registry._registerNode(node);
oper.registry.emit('node', {
eventType: 'mod',
object: node
});
-
// give event callbacks time to propagate
- await sleep(10);
+ await sleep(EVENT_PROPAGATION_DELAY);
// node is not yet synced
- sinon.assert.calledThrice(msStub);
- sinon.assert.calledThrice(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'pending',
- reason: 'mayastor does not run on node "node"'
- }
- }
- });
-
- node.connect();
+ sinon.assert.calledTwice(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'mayastor does not run on node "node"'
+ });
+ expect(stubs.updateStatus.args[1][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'mayastor on node "node" is offline'
+ });
+
+ syncedStub.returns(true);
oper.registry.emit('node', {
eventType: 'mod',
object: node
});
-
// give event callbacks time to propagate
- await sleep(10);
+ await sleep(EVENT_PROPAGATION_DELAY);
// tried to create the pool but the node is a fake
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'pending',
- reason: 'Error: Broken connection to mayastor on node "node"'
- }
- }
+ sinon.assert.callCount(stubs.updateStatus, 4);
+ expect(stubs.updateStatus.args[2][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'Creating the pool'
+ });
+ expect(stubs.updateStatus.args[3][5].status).to.deep.equal({
+ state: 'error',
+ reason: 'Error: Broken connection to mayastor on node "node"'
});
});
});
describe('del event', () => {
it('should destroy a pool', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
@@ -529,32 +577,39 @@ module.exports = function () {
const destroyStub = sinon.stub(pool, 'destroy');
destroyStub.resolves();
const node = new Node('node', {}, [pool]);
- oper = await MockedPoolOperator(
- [
- createPoolResource(
- 'pool',
- 'node',
- ['/dev/sdb'],
- 'degraded',
- '',
- 100,
- 10
- )
- ],
- [node]
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdb'],
+ [],
+ 'degraded',
+ '',
+ 100,
+ 10
);
-
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "del" event
- oper.watcher.delObject('pool');
+ oper.watcher.emit('del', poolResource);
// give event callbacks time to propagate
- await sleep(10);
+ await sleep(EVENT_PROPAGATION_DELAY);
- sinon.assert.notCalled(msStub);
+ // called in response to registry new event
+ sinon.assert.notCalled(stubs.updateStatus);
sinon.assert.calledOnce(destroyStub);
- expect(oper.resource).to.not.have.key('pool');
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
});
it('should not fail if pool does not exist', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
@@ -562,22 +617,42 @@ module.exports = function () {
capacity: 100,
used: 10
});
+ const destroyStub = sinon.stub(pool, 'destroy');
+ destroyStub.resolves();
const node = new Node('node', {}, [pool]);
- oper = await MockedPoolOperator(
- [createPoolResource('pool', 'node', ['/dev/sdb'], 'OFFLINE', '')],
- [node]
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdb'],
+ [],
+ 'offline',
+ ''
);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// we create the inconsistency between k8s and real state
node.pools = [];
// trigger "del" event
- oper.watcher.delObject('pool');
-
- // called during the initial sync
- sinon.assert.calledOnce(msStub);
- expect(oper.resource).to.not.have.key('pool');
+ oper.watcher.emit('del', poolResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ // called in response to registry new event
+ sinon.assert.calledOnce(stubs.updateStatus);
+ sinon.assert.notCalled(destroyStub);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
});
it('should destroy the pool even if it is on a different node', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
@@ -589,52 +664,84 @@ module.exports = function () {
destroyStub.resolves();
const node1 = new Node('node1', {}, []);
const node2 = new Node('node2', {}, [pool]);
- oper = await MockedPoolOperator(
- [createPoolResource('pool', 'node1', ['/dev/sdb'], 'online', '')],
- [node1, node2]
+ oper = createPoolOperator([node1, node2]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node1',
+ ['/dev/sdb'],
+ [],
+ 'degraded',
+ '',
+ 100,
+ 10
);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "del" event
- oper.watcher.delObject('pool');
-
- // called during the initial sync
- sinon.assert.calledOnce(msStub);
+ oper.watcher.emit('del', poolResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
+ // called in response to registry new event
+ sinon.assert.notCalled(stubs.updateStatus);
sinon.assert.calledOnce(destroyStub);
- expect(oper.resource).to.not.have.key('pool');
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
});
- it('should delete the resource even if the destroy fails', async () => {
+ it('should not crash if the destroy fails', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
state: 'POOL_DEGRADED',
capacity: 100,
- used: 10,
- destroy: async function () {}
+ used: 10
});
const destroyStub = sinon.stub(pool, 'destroy');
destroyStub.rejects(new GrpcError(GrpcCode.INTERNAL, 'destroy failed'));
const node = new Node('node', {}, [pool]);
- oper = await MockedPoolOperator(
- [createPoolResource('pool', 'node', ['/dev/sdb'], 'DEGRADED', '')],
- [node]
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdb'],
+ [],
+ 'degraded',
+ '',
+ 100,
+ 10
);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "del" event
- oper.watcher.delObject('pool');
-
+ oper.watcher.emit('del', poolResource);
// give event callbacks time to propagate
- await sleep(10);
-
- // called during the initial sync
- sinon.assert.calledOnce(msStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+ // called in response to registry new event
+ sinon.assert.notCalled(stubs.updateStatus);
sinon.assert.calledOnce(destroyStub);
- expect(oper.resource).to.not.have.key('pool');
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
});
});
describe('mod event', () => {
it('should not do anything if pool object has not changed', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb', 'aio:///dev/sdc'],
@@ -643,36 +750,36 @@ module.exports = function () {
used: 10
});
const node = new Node('node', {}, [pool]);
- oper = await MockedPoolOperator(
- [
- createPoolResource(
- 'pool',
- 'node',
- ['/dev/sdb', '/dev/sdc'],
- 'DEGRADED',
- ''
- )
- ],
- [node]
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdb', '/dev/sdc'],
+ [],
+ 'degraded',
+ ''
);
-
- // called during the initial sync
- sinon.assert.calledOnce(msStub);
-
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "mod" event
- oper.watcher.modObject(
- createPoolResource('pool', 'node', ['/dev/sdc', '/dev/sdb'])
- );
+ oper.watcher.emit('mod', poolResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
- // called during the initial sync
- sinon.assert.calledOnce(msStub);
- // operator state
- expect(oper.resource.pool.disks).to.have.lengthOf(2);
- expect(oper.resource.pool.disks[0]).to.equal('/dev/sdb');
- expect(oper.resource.pool.disks[1]).to.equal('/dev/sdc');
+ // called in response to registry new event
+ sinon.assert.calledOnce(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
});
it('should not do anything if disks change', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
@@ -681,27 +788,38 @@ module.exports = function () {
used: 10
});
const node = new Node('node', {}, [pool]);
- oper = await MockedPoolOperator(
- [createPoolResource('pool', 'node', ['/dev/sdb'], 'DEGRADED', '')],
- [node]
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdc'],
+ [],
+ 'degraded',
+ ''
);
-
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "mod" event
- oper.watcher.modObject(
- createPoolResource('pool', 'node', ['/dev/sdc'])
- );
+ oper.watcher.emit('mod', poolResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
- // called during the initial sync
- sinon.assert.calledOnce(msStub);
+ // called in response to registry new event
+ sinon.assert.calledOnce(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
// the real state
expect(node.pools[0].disks[0]).to.equal('aio:///dev/sdb');
- // watcher state
- expect(oper.watcher.list()[0].disks[0]).to.equal('/dev/sdc');
- // operator state
- expect(oper.resource.pool.disks[0]).to.equal('/dev/sdc');
});
it('should not do anything if node changes', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
@@ -711,31 +829,38 @@ module.exports = function () {
});
const node1 = new Node('node1', {}, [pool]);
const node2 = new Node('node2', {}, []);
- oper = await MockedPoolOperator(
- [createPoolResource('pool', 'node1', ['/dev/sdb'], 'DEGRADED', '')],
- [node1]
+ oper = createPoolOperator([node1, node2]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node2',
+ ['/dev/sdb'],
+ [],
+ 'degraded',
+ ''
);
-
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
// trigger "mod" event
- oper.watcher.modObject(
- createPoolResource('pool', 'node2', ['/dev/sdb'])
- );
+ oper.watcher.emit('mod', poolResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
- // called during the initial sync
- sinon.assert.calledOnce(msStub);
- // the real state
- expect(node1.pools).to.have.lengthOf(1);
- expect(node2.pools).to.have.lengthOf(0);
- // watcher state
- expect(oper.watcher.list()[0].node).to.equal('node2');
- // operator state
- expect(oper.resource.pool.node).to.equal('node2');
+ // called in response to registry new event
+ sinon.assert.calledOnce(stubs.updateStatus);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
});
});
});
describe('node events', () => {
- var oper; // pool operator
+ let oper; // pool operator
afterEach(async () => {
if (oper) {
@@ -745,39 +870,125 @@ module.exports = function () {
});
it('should create pool upon node sync event if it does not exist', async () => {
+ let stubs;
+ const pool = new Pool({
+ name: 'pool',
+ disks: ['aio:///dev/sdb'],
+ state: 'POOL_DEGRADED',
+ capacity: 100,
+ used: 10
+ });
const node = new Node('node', {}, []);
const createPoolStub = sinon.stub(node, 'createPool');
- createPoolStub.resolves(
- new Pool({
- name: 'pool',
- node: node,
- disks: ['aio:///dev/sdb'],
- state: 'POOL_ONLINE',
- capacity: 100,
- used: 4
- })
+ const isSyncedStub = sinon.stub(node, 'isSynced');
+ createPoolStub.resolves(pool);
+ isSyncedStub.onCall(0).returns(false);
+ isSyncedStub.onCall(1).returns(true);
+ oper = createPoolOperator([node]);
+ const poolResource1 = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdb'],
+ [],
+ 'degraded',
+ ''
);
- oper = await MockedPoolOperator(
- [createPoolResource('pool', 'node', ['/dev/sdb'])],
- [node]
+ const poolResource2 = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdb'],
+ [],
+ 'pending',
+ 'mayastor on node "node" is offline'
);
-
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'pending',
- reason: 'Creating the pool'
- }
- }
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.onCall(0).returns(poolResource1);
+ stubs.get.onCall(1).returns(poolResource2);
+ stubs.list.returns([poolResource1]);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ oper.registry.emit('node', {
+ eventType: 'sync',
+ object: node
+ });
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledTwice(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'mayastor on node "node" is offline'
+ });
+ expect(stubs.updateStatus.args[1][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'Creating the pool'
});
sinon.assert.calledOnce(createPoolStub);
sinon.assert.calledWith(createPoolStub, 'pool', ['/dev/sdb']);
});
- it('should not create pool upon node sync event if it exists', async () => {
+ it('should add finalizer for new pool resource', async () => {
+ let stubs;
+ const pool = new Pool({
+ name: 'pool',
+ disks: ['aio:///dev/sdb'],
+ state: 'POOL_ONLINE',
+ capacity: 100,
+ used: 4
+ });
+ // replica will trigger finalizer
+ const replica1 = new Replica({ uuid: 'UUID1' });
+ const replica2 = new Replica({ uuid: 'UUID2' });
+ replica1.pool = pool;
+ pool.replicas = [replica1];
+ const node = new Node('node', {}, [pool]);
+ oper = createPoolOperator([node]);
+
+ const poolResource = createK8sPoolResource(
+ 'pool',
+ 'node1',
+ ['/dev/sdb'],
+ [],
+ 'online',
+ '',
+ 100,
+ 4
+ );
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ stubs.update.resolves();
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.calledOnce(stubs.update);
+ expect(stubs.update.args[0][5].metadata.finalizers).to.deep.equal([
+ 'finalizer.mayastor.openebs.io'
+ ]);
+
+ // add a second replica - should not change anything
+ pool.replicas.push(replica2);
+ oper.registry.emit('replica', {
+ eventType: 'new',
+ object: replica2
+ });
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.calledOnce(stubs.update);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.updateStatus);
+ });
+
+ it('should remove finalizer when last replica is removed', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
@@ -785,36 +996,101 @@ module.exports = function () {
capacity: 100,
used: 4
});
+ const replica1 = new Replica({ uuid: 'UUID1' });
+ const replica2 = new Replica({ uuid: 'UUID2' });
+ pool.replicas = [replica1, replica2];
+ replica1.pool = pool;
+ replica2.pool = pool;
+ const node = new Node('node', {}, [pool]);
+ oper = createPoolOperator([node]);
+
+ const poolResource = createK8sPoolResource(
+ 'pool',
+ 'node1',
+ ['/dev/sdb'],
+ ['finalizer.mayastor.openebs.io'],
+ 'online',
+ '',
+ 100,
+ 4
+ );
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ stubs.update.resolves();
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.update);
+ pool.replicas.splice(1, 1);
+ oper.registry.emit('replica', {
+ eventType: 'del',
+ object: replica2
+ });
+ await sleep(EVENT_PROPAGATION_DELAY);
+ sinon.assert.notCalled(stubs.update);
+ pool.replicas = [];
+ oper.registry.emit('replica', {
+ eventType: 'del',
+ object: replica1
+ });
+ await sleep(EVENT_PROPAGATION_DELAY);
+ sinon.assert.calledOnce(stubs.update);
+ expect(stubs.update.args[0][5].metadata.finalizers).to.have.lengthOf(0);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.updateStatus);
+ });
+
+ it('should not create pool upon node sync event if it exists', async () => {
+ let stubs;
+ const pool = new Pool({
+ name: 'pool',
+ disks: ['aio:///dev/sdb'],
+ state: 'POOL_DEGRADED',
+ capacity: 100,
+ used: 10
+ });
const node = new Node('node', {}, [pool]);
const createPoolStub = sinon.stub(node, 'createPool');
createPoolStub.resolves(pool);
- oper = await MockedPoolOperator(
- [
- createPoolResource(
- 'pool',
- 'node',
- ['/dev/sdb'],
- 'online',
- '',
- 100,
- 4
- )
- ],
- [node]
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdb'],
+ [],
+ 'degraded',
+ '',
+ 100,
+ 10
);
-
- sinon.assert.notCalled(msStub);
- sinon.assert.notCalled(putStub);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ stubs.list.returns([poolResource]);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.updateStatus);
sinon.assert.notCalled(createPoolStub);
});
it('should not create pool upon node sync event if it exists on another node', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
- state: 'POOL_ONLINE',
+ state: 'POOL_DEGRADED',
capacity: 100,
- used: 4
+ used: 10
});
const node1 = new Node('node1', {}, []);
const node2 = new Node('node2', {}, [pool]);
@@ -822,47 +1098,64 @@ module.exports = function () {
const createPoolStub2 = sinon.stub(node2, 'createPool');
createPoolStub1.resolves(pool);
createPoolStub2.resolves(pool);
- oper = await MockedPoolOperator(
- [
- createPoolResource(
- 'pool',
- 'node1',
- ['/dev/sdb'],
- 'online',
- '',
- 100,
- 4
- )
- ],
- [node1, node2]
+ oper = createPoolOperator([node1, node2]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node1',
+ ['/dev/sdb'],
+ [],
+ 'degraded',
+ '',
+ 100,
+ 10
);
-
- sinon.assert.notCalled(msStub);
- sinon.assert.notCalled(putStub);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ stubs.list.returns([poolResource]);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.updateStatus);
sinon.assert.notCalled(createPoolStub1);
sinon.assert.notCalled(createPoolStub2);
});
it('should remove pool upon pool new event if there is no pool resource', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
state: 'POOL_ONLINE',
capacity: 100,
- used: 4,
- destroy: async function () {}
+ used: 4
});
const destroyStub = sinon.stub(pool, 'destroy');
destroyStub.resolves();
const node = new Node('node', {}, [pool]);
- oper = await MockedPoolOperator([], [node]);
+ oper = createPoolOperator([node]);
- sinon.assert.notCalled(msStub);
- sinon.assert.notCalled(putStub);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.updateStatus);
sinon.assert.calledOnce(destroyStub);
});
it('should update resource properties upon pool mod event', async () => {
+ let stubs;
const offlineReason = 'mayastor does not run on the node "node"';
const pool = new Pool({
name: 'pool',
@@ -872,70 +1165,84 @@ module.exports = function () {
used: 4
});
const node = new Node('node', {}, [pool]);
- oper = await MockedPoolOperator(
- [
- createPoolResource(
- 'pool',
- 'node',
- ['/dev/sdb'],
- 'online',
- '',
- 100,
- 4
- )
- ],
- [node]
+ oper = createPoolOperator([node]);
+
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node1',
+ ['/dev/sdb'],
+ [],
+ 'online',
+ '',
+ 100,
+ 4
);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
- pool.state = 'POOL_OFFLINE';
// simulate pool mod event
+ pool.state = 'POOL_OFFLINE';
oper.registry.emit('pool', {
eventType: 'mod',
object: pool
});
-
// Give event time to propagate
- await sleep(10);
-
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'offline',
- reason: offlineReason
- }
- }
- });
- expect(oper.watcher.objects.pool.status.state).to.equal('offline');
- expect(oper.watcher.objects.pool.status.reason).to.equal(offlineReason);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'offline',
+ reason: offlineReason,
+ capacity: 100,
+ disks: ['aio:///dev/sdb'],
+ used: 4
+ });
});
it('should ignore pool mod event if pool resource does not exist', async () => {
- const node = new Node('node', {}, []);
- oper = await MockedPoolOperator([], [node]);
+ let stubs;
+ const pool = new Pool({
+ name: 'pool',
+ disks: ['aio:///dev/sdb'],
+ state: 'POOL_ONLINE',
+ capacity: 100,
+ used: 4
+ });
+ const node = new Node('node', {}, [pool]);
+ oper = createPoolOperator([node]);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ // simulate pool mod event
+ pool.state = 'POOL_OFFLINE';
oper.registry.emit('pool', {
eventType: 'mod',
- object: new Pool({
- name: 'pool',
- disks: ['aio:///dev/sdb'],
- state: 'POOL_OFFLINE',
- capacity: 100,
- used: 4
- })
+ object: pool
});
-
// Give event time to propagate
- await sleep(10);
+ await sleep(EVENT_PROPAGATION_DELAY);
- sinon.assert.notCalled(msStub);
- sinon.assert.notCalled(putStub);
- expect(oper.resource.pool).to.be.undefined();
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.updateStatus);
});
it('should create pool upon pool del event if pool resource exist', async () => {
+ let stubs;
const pool = new Pool({
name: 'pool',
disks: ['aio:///dev/sdb'],
@@ -946,22 +1253,24 @@ module.exports = function () {
const node = new Node('node', {}, [pool]);
const createPoolStub = sinon.stub(node, 'createPool');
createPoolStub.resolves(pool);
- oper = await MockedPoolOperator(
- [
- createPoolResource(
- 'pool',
- 'node',
- ['/dev/sdb'],
- 'online',
- '',
- 100,
- 4
- )
- ],
- [node]
+ oper = createPoolOperator([node]);
+ const poolResource = createPoolResource(
+ 'pool',
+ 'node',
+ ['/dev/sdb'],
+ [],
+ 'online',
+ '',
+ 100,
+ 4
);
-
- sinon.assert.notCalled(msStub);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ stubs.get.returns(poolResource);
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
sinon.assert.notCalled(createPoolStub);
node.pools = [];
@@ -969,43 +1278,51 @@ module.exports = function () {
eventType: 'del',
object: pool
});
-
// Give event time to propagate
- await sleep(10);
-
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, 'pool');
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- status: {
- state: 'pending',
- reason: 'Creating the pool'
- }
- }
- });
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.calledOnce(createPoolStub);
sinon.assert.calledWith(createPoolStub, 'pool', ['/dev/sdb']);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ state: 'pending',
+ reason: 'Creating the pool'
+ });
});
it('should ignore pool del event if pool resource does not exist', async () => {
+ let stubs;
+ const pool = new Pool({
+ name: 'pool',
+ disks: ['aio:///dev/sdb'],
+ state: 'POOL_ONLINE',
+ capacity: 100,
+ used: 4
+ });
const node = new Node('node', {}, []);
- oper = await MockedPoolOperator([], [node]);
+ oper = createPoolOperator([node]);
+ mockCache(oper.watcher, (arg) => {
+ stubs = arg;
+ });
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ node.pools = [];
oper.registry.emit('pool', {
eventType: 'del',
- object: new Pool({
- name: 'pool',
- disks: ['aio:///dev/sdb'],
- state: 'POOL_ONLINE',
- capacity: 100,
- used: 4
- })
+ object: pool
});
-
// Give event time to propagate
- await sleep(10);
- sinon.assert.notCalled(msStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.updateStatus);
});
});
};
diff --git a/csi/moac/test/volume_operator_test.js b/csi/moac/test/volume_operator_test.js
index 15a0eec01..9bdbb0f43 100644
--- a/csi/moac/test/volume_operator_test.js
+++ b/csi/moac/test/volume_operator_test.js
@@ -1,10 +1,4 @@
// Unit tests for the volume operator
-//
-// We don't test the init method which depends on k8s api client and watcher.
-// That method *must* be tested manually and in real k8s environment. For the
-// rest of the dependencies we provide fake objects which mimic the real
-// behaviour and allow us to test volume operator in isolation from other
-// components.
'use strict';
@@ -12,15 +6,33 @@ const _ = require('lodash');
const expect = require('chai').expect;
const sinon = require('sinon');
const sleep = require('sleep-promise');
+const { KubeConfig } = require('client-node-fixed-watcher');
const Registry = require('../registry');
const Volume = require('../volume');
const Volumes = require('../volumes');
-const VolumeOperator = require('../volume_operator');
+const { VolumeOperator, VolumeResource } = require('../volume_operator');
const { GrpcError, GrpcCode } = require('../grpc_client');
-const Watcher = require('./watcher_stub');
+const { mockCache } = require('./watcher_stub');
const UUID = 'd01b8bfb-0116-47b0-a03a-447fcbdc0e99';
const NAMESPACE = 'mayastor';
+const EVENT_PROPAGATION_DELAY = 10;
+
+const fakeConfig = {
+ clusters: [
+ {
+ name: 'cluster',
+ server: 'foo.company.com'
+ }
+ ],
+ contexts: [
+ {
+ cluster: 'cluster',
+ user: 'user'
+ }
+ ],
+ users: [{ name: 'user' }]
+};
function defaultMeta (uuid) {
return {
@@ -34,122 +46,75 @@ function defaultMeta (uuid) {
};
}
-module.exports = function () {
- var msStub, putStub, putStatusStub, deleteStub, postStub;
- var defaultSpec = {
- replicaCount: 1,
- preferredNodes: ['node1', 'node2'],
- requiredNodes: ['node2'],
- requiredBytes: 100,
- limitBytes: 120,
- protocol: 'nbd'
- };
- var defaultStatus = {
- size: 110,
- node: 'node2',
- state: 'healthy',
- nexus: {
- deviceUri: 'file:///dev/nbd0',
- state: 'NEXUS_ONLINE',
- children: [
- {
- uri: 'bdev:///' + UUID,
- state: 'CHILD_ONLINE'
- }
- ]
- },
- replicas: [
+const defaultSpec = {
+ replicaCount: 1,
+ preferredNodes: ['node1', 'node2'],
+ requiredNodes: ['node2'],
+ requiredBytes: 100,
+ limitBytes: 120,
+ protocol: 'nbd'
+};
+
+const defaultStatus = {
+ size: 110,
+ node: 'node2',
+ state: 'healthy',
+ nexus: {
+ deviceUri: 'file:///dev/nbd0',
+ state: 'NEXUS_ONLINE',
+ children: [
{
uri: 'bdev:///' + UUID,
- node: 'node2',
- pool: 'pool',
- offline: false
+ state: 'CHILD_ONLINE'
}
]
- };
-
- // Create k8s volume resource object
- function createVolumeResource (uuid, spec, status) {
- const obj = {
- apiVersion: 'openebs.io/v1alpha1',
- kind: 'MayastorVolume',
- metadata: defaultMeta(uuid),
- spec: spec
- };
- if (status) {
- obj.status = status;
+ },
+ replicas: [
+ {
+ uri: 'bdev:///' + UUID,
+ node: 'node2',
+ pool: 'pool',
+ offline: false
}
- return obj;
- }
+ ]
+};
- // k8s api client stub.
- //
- // Note that this stub serves only for PUT method on mayastor resource
- // endpoint to update the status of resource. Fake watcher that is used
- // in the tests does not use this client stub.
- function createK8sClient (watcher) {
- const mayastorvolumes = { mayastorvolumes: function (name) {} };
- const namespaces = function (ns) {
- expect(ns).to.equal(NAMESPACE);
- return mayastorvolumes;
- };
- const client = {
- apis: {
- 'openebs.io': {
- v1alpha1: { namespaces }
- }
- }
- };
-
- msStub = sinon.stub(mayastorvolumes, 'mayastorvolumes');
- msStub.post = async function (payload) {
- watcher.objects[payload.body.metadata.name] = payload.body;
- // simulate the asynchronicity of the put
- await sleep(1);
- };
- postStub = sinon.stub(msStub, 'post');
- postStub.callThrough();
-
- const msObject = {
- // the tricky thing here is that we have to update watcher's cache
- // if we use this fake k8s client to change the object in order to
- // mimic real behaviour.
- put: async function (payload) {
- watcher.objects[payload.body.metadata.name].spec = payload.body.spec;
- },
- delete: async function () {},
- status: {
- put: async function (payload) {
- watcher.objects[payload.body.metadata.name].status =
- payload.body.status;
- }
- }
- };
- putStub = sinon.stub(msObject, 'put');
- putStub.callThrough();
- putStatusStub = sinon.stub(msObject.status, 'put');
- putStatusStub.callThrough();
- deleteStub = sinon.stub(msObject, 'delete');
- deleteStub.callThrough();
- msStub.returns(msObject);
- return client;
+// Create k8s volume resource object
+function createK8sVolumeResource (uuid, spec, status) {
+ const obj = {
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorVolume',
+ metadata: defaultMeta(uuid),
+ spec: spec
+ };
+ if (status) {
+ obj.status = status;
}
+ return obj;
+}
- // Create a pool operator object suitable for testing - with fake watcher
- // and fake k8s api client.
- async function mockedVolumeOperator (k8sObjects, volumes) {
- const oper = new VolumeOperator(NAMESPACE);
- oper.volumes = volumes;
- oper.watcher = new Watcher(oper._filterMayastorVolume, k8sObjects);
- oper.k8sClient = createK8sClient(oper.watcher);
+// Create volume resource object
+function createVolumeResource (uuid, spec, status) {
+ return new VolumeResource(createK8sVolumeResource(uuid, spec, status));
+}
- await oper.start();
- return oper;
- }
+// Create a pool operator object suitable for testing - with fake watcher
+// and fake k8s api client.
+async function createVolumeOperator (volumes, stubsCb) {
+ const kc = new KubeConfig();
+ Object.assign(kc, fakeConfig);
+ const oper = new VolumeOperator(NAMESPACE, kc, volumes);
+ mockCache(oper.watcher, stubsCb);
+ await oper.start();
+ // give time to registry to install its callbacks
+ await sleep(EVENT_PROPAGATION_DELAY);
+ return oper;
+}
- describe('resource filter', () => {
- it('valid mayastor volume with status should pass the filter', () => {
- const obj = createVolumeResource(
+module.exports = function () {
+ describe('VolumeResource constructor', () => {
+ it('should create mayastor volume with status', () => {
+ const res = createVolumeResource(
UUID,
{
replicaCount: 3,
@@ -182,8 +147,6 @@ module.exports = function () {
]
}
);
-
- const res = VolumeOperator.prototype._filterMayastorVolume(obj);
expect(res.metadata.name).to.equal(UUID);
expect(res.spec.replicaCount).to.equal(3);
expect(res.spec.preferredNodes).to.have.lengthOf(2);
@@ -208,8 +171,28 @@ module.exports = function () {
expect(res.status.replicas[0].offline).to.equal(false);
});
- it('valid mayastor volume with status without nexus should pass the filter', () => {
- const obj = createVolumeResource(
+ it('should create mayastor volume with unknown state', () => {
+ const res = createVolumeResource(
+ UUID,
+ {
+ replicaCount: 1,
+ requiredBytes: 100
+ },
+ {
+ size: 100,
+ node: 'node2',
+ state: 'online' // "online" is not a valid volume state
+ }
+ );
+ expect(res.metadata.name).to.equal(UUID);
+ expect(res.spec.replicaCount).to.equal(1);
+ expect(res.status.size).to.equal(100);
+ expect(res.status.node).to.equal('node2');
+ expect(res.status.state).to.equal('unknown');
+ });
+
+ it('should create mayastor volume with status without nexus', () => {
+ const res = createVolumeResource(
UUID,
{
replicaCount: 3,
@@ -226,7 +209,6 @@ module.exports = function () {
}
);
- const res = VolumeOperator.prototype._filterMayastorVolume(obj);
expect(res.metadata.name).to.equal(UUID);
expect(res.spec.replicaCount).to.equal(3);
expect(res.spec.preferredNodes).to.have.lengthOf(2);
@@ -243,25 +225,23 @@ module.exports = function () {
expect(res.status.replicas).to.have.lengthOf(0);
});
- it('valid mayastor volume without status should pass the filter', () => {
- const obj = createVolumeResource(UUID, {
+ it('should create mayastor volume without status', () => {
+ const res = createVolumeResource(UUID, {
replicaCount: 3,
preferredNodes: ['node1', 'node2'],
requiredNodes: ['node2'],
requiredBytes: 100,
limitBytes: 120
});
- const res = VolumeOperator.prototype._filterMayastorVolume(obj);
expect(res.metadata.name).to.equal(UUID);
expect(res.spec.replicaCount).to.equal(3);
expect(res.status).to.be.undefined();
});
- it('mayastor volume without optional parameters should pass the filter', () => {
- const obj = createVolumeResource(UUID, {
+ it('should create mayastor volume without optional parameters', () => {
+ const res = createVolumeResource(UUID, {
requiredBytes: 100
});
- const res = VolumeOperator.prototype._filterMayastorVolume(obj);
expect(res.metadata.name).to.equal(UUID);
expect(res.spec.replicaCount).to.equal(1);
expect(res.spec.preferredNodes).to.have.lengthOf(0);
@@ -271,32 +251,76 @@ module.exports = function () {
expect(res.status).to.be.undefined();
});
- it('mayastor volume without requiredSize should be ignored', () => {
- const obj = createVolumeResource(UUID, {
+ it('should throw if requiredSize is missing', () => {
+ expect(() => createVolumeResource(UUID, {
replicaCount: 3,
preferredNodes: ['node1', 'node2'],
requiredNodes: ['node2'],
limitBytes: 120
- });
- const res = VolumeOperator.prototype._filterMayastorVolume(obj);
- expect(res).to.be.null();
+ })).to.throw();
});
- it('mayastor volume with invalid UUID should be ignored', () => {
- const obj = createVolumeResource('blabla', {
+ it('should throw if UUID is invalid', () => {
+ expect(() => createVolumeResource('blabla', {
replicaCount: 3,
preferredNodes: ['node1', 'node2'],
requiredNodes: ['node2'],
requiredBytes: 100,
limitBytes: 120
+ })).to.throw();
+ });
+ });
+
+ describe('init method', () => {
+ let kc, oper, fakeApiStub;
+
+ beforeEach(() => {
+ const registry = new Registry();
+ kc = new KubeConfig();
+ Object.assign(kc, fakeConfig);
+ oper = new VolumeOperator(NAMESPACE, kc, registry);
+ const makeApiStub = sinon.stub(kc, 'makeApiClient');
+ const fakeApi = {
+ createCustomResourceDefinition: () => null
+ };
+ fakeApiStub = sinon.stub(fakeApi, 'createCustomResourceDefinition');
+ makeApiStub.returns(fakeApi);
+ });
+
+ afterEach(() => {
+ if (oper) {
+ oper.stop();
+ oper = undefined;
+ }
+ });
+
+ it('should create CRD if it does not exist', async () => {
+ fakeApiStub.resolves();
+ await oper.init(kc);
+ });
+
+ it('should ignore error if CRD already exists', async () => {
+ fakeApiStub.rejects({
+ statusCode: 409
});
- const res = VolumeOperator.prototype._filterMayastorVolume(obj);
- expect(res).to.be.null();
+ await oper.init(kc);
+ });
+
+ it('should throw if CRD creation fails', async () => {
+ fakeApiStub.rejects({
+ statusCode: 404
+ });
+ try {
+ await oper.init(kc);
+ } catch (err) {
+ return;
+ }
+ throw new Error('Init did not fail');
});
});
describe('watcher events', () => {
- var oper; // volume operator
+ let oper; // volume operator
afterEach(async () => {
if (oper) {
@@ -306,63 +330,29 @@ module.exports = function () {
});
it('should call import volume for existing resources when starting the operator', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const importVolumeStub = sinon.stub(volumes, 'importVolume');
// return value is not used so just return something
importVolumeStub.resolves({ uuid: UUID });
- oper = await mockedVolumeOperator(
- [createVolumeResource(UUID, defaultSpec, defaultStatus)],
- volumes
- );
- sinon.assert.calledOnce(importVolumeStub);
- sinon.assert.calledWith(importVolumeStub, UUID, defaultSpec);
- });
-
- it('should import volume upon "new" event', async () => {
- const registry = new Registry();
- const volumes = new Volumes(registry);
- const defaultStatus =
- {
- node: 'ksnode-1',
- replicas: [],
- size: 1024,
- state: 'healthy'
- };
-
- const importVolumeStub = sinon.stub(volumes, 'importVolume');
- importVolumeStub.resolves({ uuid: UUID });
-
- oper = await mockedVolumeOperator([], volumes);
+ const volumeResource = createVolumeResource(UUID, defaultSpec, defaultStatus);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ });
// trigger "new" event
- oper.watcher.newObject(createVolumeResource(UUID, defaultSpec, defaultStatus));
- sinon.assert.calledOnce(importVolumeStub);
- sinon.assert.calledWith(importVolumeStub, UUID, defaultSpec, defaultStatus);
- });
-
- it('should not try to import volume upon "new" event if the resource was self-created', async () => {
- const registry = new Registry();
- const volumes = new Volumes(registry);
- sinon.stub(volumes, 'get').returns([]);
- const importVolumeStub = sinon.stub(volumes, 'importVolume');
- importVolumeStub.resolves({ uuid: UUID });
+ oper.watcher.emit('new', volumeResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
- oper = await mockedVolumeOperator([], volumes);
- // Pretend the volume creation through i.e. CSI.
- await sleep(10);
- const volume = new Volume(UUID, registry, defaultSpec);
- volumes.emit('volume', {
- eventType: 'new',
- object: volume
- });
- await sleep(10);
- // now trigger "new" watcher event (natural consequence of the above)
- oper.watcher.newObject(createVolumeResource(UUID, defaultSpec));
- sinon.assert.notCalled(importVolumeStub);
+ sinon.assert.calledOnce(importVolumeStub);
+ sinon.assert.calledWith(importVolumeStub, UUID, defaultSpec);
});
it('should set reason in resource if volume import fails upon "new" event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const importVolumeStub = sinon.stub(volumes, 'importVolume');
@@ -370,60 +360,72 @@ module.exports = function () {
new GrpcError(GrpcCode.INTERNAL, 'create failed')
);
- oper = await mockedVolumeOperator([], volumes);
+ const volumeResource = createVolumeResource(UUID, defaultSpec, defaultStatus);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ });
// trigger "new" event
- oper.watcher.newObject(createVolumeResource(UUID, defaultSpec));
- await sleep(10);
+ oper.watcher.emit('new', volumeResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.calledOnce(importVolumeStub);
- sinon.assert.calledOnce(msStub);
- sinon.assert.calledWith(msStub, UUID);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.calledOnce(putStatusStub);
- sinon.assert.calledWithMatch(putStatusStub, {
- body: {
- metadata: defaultMeta(UUID),
- status: {
- state: 'pending',
- reason: 'Error: create failed'
- }
- }
- });
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status.state).to.equal('error');
+ expect(stubs.updateStatus.args[0][5].status.reason).to.equal('Error: create failed');
});
it('should destroy the volume upon "del" event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const destroyVolumeStub = sinon.stub(volumes, 'destroyVolume');
destroyVolumeStub.resolves();
- const obj = createVolumeResource(UUID, defaultSpec, defaultStatus);
+ const volumeResource = createVolumeResource(UUID, defaultSpec, defaultStatus);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(obj);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ });
+ const getVolumeStub = sinon.stub(volumes, 'get');
+ getVolumeStub.returns({ uuid: UUID });
// trigger "del" event
- oper.watcher.delObject(UUID);
+ oper.watcher.emit('del', volumeResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.calledOnce(destroyVolumeStub);
sinon.assert.calledWith(destroyVolumeStub, UUID);
});
it('should handle gracefully if destroy of a volume fails upon "del" event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const destroyVolumeStub = sinon.stub(volumes, 'destroyVolume');
destroyVolumeStub.rejects(
new GrpcError(GrpcCode.INTERNAL, 'destroy failed')
);
- const obj = createVolumeResource(UUID, defaultSpec, defaultStatus);
+ const volumeResource = createVolumeResource(UUID, defaultSpec, defaultStatus);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(obj);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ });
+ const getVolumeStub = sinon.stub(volumes, 'get');
+ getVolumeStub.returns({ uuid: UUID });
// trigger "del" event
- oper.watcher.delObject(UUID);
+ oper.watcher.emit('del', volumeResource);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.calledOnce(destroyVolumeStub);
sinon.assert.calledWith(destroyVolumeStub, UUID);
});
it('should modify the volume upon "mod" event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const volume = new Volume(UUID, registry, defaultSpec);
@@ -451,10 +453,14 @@ module.exports = function () {
defaultStatus
);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(oldObj);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(oldObj);
+ });
// trigger "mod" event
- oper.watcher.modObject(newObj);
+ oper.watcher.emit('mod', newObj);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
sinon.assert.calledOnce(fsaStub);
expect(volume.replicaCount).to.equal(3);
@@ -465,6 +471,7 @@ module.exports = function () {
});
it('should not crash if update volume fails upon "mod" event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const volume = new Volume(UUID, registry, defaultSpec);
@@ -492,10 +499,14 @@ module.exports = function () {
defaultStatus
);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(oldObj);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(oldObj);
+ });
// trigger "mod" event
- oper.watcher.modObject(newObj);
+ oper.watcher.emit('mod', newObj);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
sinon.assert.notCalled(fsaStub);
expect(volume.replicaCount).to.equal(1);
@@ -504,6 +515,7 @@ module.exports = function () {
});
it('should not do anything if volume params stay the same upon "mod" event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const volume = new Volume(UUID, registry, defaultSpec);
@@ -520,16 +532,21 @@ module.exports = function () {
// new specification of the object that is the same
const newObj = createVolumeResource(UUID, defaultSpec, defaultStatus);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(oldObj);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(oldObj);
+ });
// trigger "mod" event
- oper.watcher.modObject(newObj);
+ oper.watcher.emit('mod', newObj);
+ // give event callbacks time to propagate
+ await sleep(EVENT_PROPAGATION_DELAY);
+
sinon.assert.notCalled(fsaStub);
});
});
describe('volume events', () => {
- var oper; // volume operator
+ let oper; // volume operator
afterEach(async () => {
if (oper) {
@@ -539,8 +556,9 @@ module.exports = function () {
});
it('should create a resource upon "new" volume event', async () => {
+ let stubs;
const registry = new Registry();
- const volume = new Volume(UUID, registry, defaultSpec);
+ const volume = new Volume(UUID, registry, defaultSpec, 100);
const volumes = new Volumes(registry);
sinon
.stub(volumes, 'get')
@@ -549,53 +567,55 @@ module.exports = function () {
.withArgs()
.returns([volume]);
- oper = await mockedVolumeOperator([], volumes);
-
- await sleep(20);
- sinon.assert.calledOnce(postStub);
- sinon.assert.calledWithMatch(postStub, {
- body: {
- metadata: {
- name: UUID,
- namespace: NAMESPACE
- },
- spec: defaultSpec
- }
+ const volumeResource = createVolumeResource(UUID, defaultSpec);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.onFirstCall().returns();
+ stubs.get.onSecondCall().returns(volumeResource);
+ stubs.create.resolves();
+ stubs.updateStatus.resolves();
});
- sinon.assert.calledOnce(putStatusStub);
- sinon.assert.calledWithMatch(putStatusStub, {
- body: {
- status: {
- node: '',
- reason: '',
- replicas: [],
- size: 0,
- state: 'pending'
- }
- }
+
+ sinon.assert.calledOnce(stubs.create);
+ expect(stubs.create.args[0][4].metadata.name).to.equal(UUID);
+ expect(stubs.create.args[0][4].metadata.namespace).to.equal(NAMESPACE);
+ expect(stubs.create.args[0][4].spec).to.deep.equal(defaultSpec);
+ sinon.assert.calledOnce(stubs.updateStatus);
+ expect(stubs.updateStatus.args[0][5].status).to.deep.equal({
+ node: '',
+ replicas: [],
+ size: 100,
+ state: 'pending'
});
});
it('should not crash if POST fails upon "new" volume event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const volume = new Volume(UUID, registry, defaultSpec);
sinon.stub(volumes, 'get').returns([]);
- oper = await mockedVolumeOperator([], volumes);
- postStub.rejects(new Error('post failed'));
- // we have to sleep to give event stream chance to register its handlers
- await sleep(10);
+ const volumeResource = createVolumeResource(UUID, defaultSpec);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.onFirstCall().returns();
+ stubs.get.onSecondCall().returns(volumeResource);
+ stubs.create.rejects(new Error('POST failed'));
+ stubs.updateStatus.resolves();
+ });
+
volumes.emit('volume', {
eventType: 'new',
object: volume
});
- await sleep(10);
- sinon.assert.calledOnce(postStub);
- sinon.assert.notCalled(putStatusStub);
+ await sleep(EVENT_PROPAGATION_DELAY);
+ sinon.assert.calledOnce(stubs.create);
+ sinon.assert.notCalled(stubs.updateStatus);
});
it('should update the resource upon "new" volume event if it exists', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
const newSpec = _.cloneDeep(defaultSpec);
@@ -608,23 +628,25 @@ module.exports = function () {
.withArgs()
.returns([volume]);
- oper = await mockedVolumeOperator([], volumes);
- const obj = createVolumeResource(UUID, defaultSpec);
- oper.watcher.injectObject(obj);
-
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: { spec: newSpec }
+ const volumeResource = createVolumeResource(UUID, defaultSpec);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ stubs.update.resolves();
+ stubs.updateStatus.resolves();
});
- sinon.assert.calledOnce(putStatusStub);
+
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.calledOnce(stubs.update);
+ expect(stubs.update.args[0][5].spec).to.deep.equal(newSpec);
+ sinon.assert.calledOnce(stubs.updateStatus);
});
it('should not update the resource upon "new" volume event if it is the same', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
- const volume = new Volume(UUID, registry, defaultSpec);
+ const volume = new Volume(UUID, registry, defaultSpec, 100);
sinon
.stub(volumes, 'get')
.withArgs(UUID)
@@ -632,32 +654,37 @@ module.exports = function () {
.withArgs()
.returns([volume]);
- oper = await mockedVolumeOperator([], volumes);
- const obj = createVolumeResource(UUID, defaultSpec, {
- size: 0,
+ const volumeResource = createVolumeResource(UUID, defaultSpec, {
+ size: 100,
node: '',
state: 'pending',
- reason: '',
replicas: []
});
- oper.watcher.injectObject(obj);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ stubs.update.resolves();
+ stubs.updateStatus.resolves();
+ });
- await sleep(10);
- sinon.assert.notCalled(putStub);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStatusStub);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.updateStatus);
});
it('should update the resource upon "mod" volume event', async () => {
- const obj = createVolumeResource(UUID, defaultSpec);
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
sinon.stub(volumes, 'get').returns([]);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(obj);
- // we have to sleep to give event stream chance to register its handlers
- await sleep(10);
+ const volumeResource = createVolumeResource(UUID, defaultSpec);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ stubs.update.resolves();
+ stubs.updateStatus.resolves();
+ });
const newSpec = {
replicaCount: 3,
@@ -672,51 +699,51 @@ module.exports = function () {
eventType: 'mod',
object: volume
});
+ await sleep(EVENT_PROPAGATION_DELAY);
- await sleep(10);
- sinon.assert.calledOnce(putStub);
- sinon.assert.calledWithMatch(putStub, {
- body: {
- metadata: defaultMeta(UUID),
- spec: newSpec
- }
- });
- sinon.assert.calledOnce(putStatusStub);
+ sinon.assert.calledOnce(stubs.update);
+ expect(stubs.update.args[0][5].spec).to.deep.equal(newSpec);
+ sinon.assert.calledOnce(stubs.updateStatus);
});
it('should update just the status if spec has not changed upon "mod" volume event', async () => {
- const obj = createVolumeResource(UUID, defaultSpec);
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
sinon.stub(volumes, 'get').returns([]);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(obj);
- // we have to sleep to give event stream chance to register its handlers
- await sleep(10);
+ const volumeResource = createVolumeResource(UUID, defaultSpec);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ stubs.update.resolves();
+ stubs.updateStatus.resolves();
+ });
const volume = new Volume(UUID, registry, defaultSpec);
volumes.emit('volume', {
eventType: 'mod',
object: volume
});
+ await sleep(EVENT_PROPAGATION_DELAY);
- await sleep(10);
- sinon.assert.notCalled(putStub);
- sinon.assert.calledOnce(putStatusStub);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
});
it('should not crash if PUT fails upon "mod" volume event', async () => {
- const obj = createVolumeResource(UUID, defaultSpec);
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
sinon.stub(volumes, 'get').returns([]);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(obj);
- putStub.rejects(new Error('put failed'));
- // we have to sleep to give event stream chance to register its handlers
- await sleep(10);
+ const volumeResource = createVolumeResource(UUID, defaultSpec);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ stubs.update.rejects(new Error('PUT failed'));
+ stubs.updateStatus.resolves();
+ });
const newSpec = {
replicaCount: 3,
@@ -731,20 +758,22 @@ module.exports = function () {
eventType: 'mod',
object: volume
});
+ await sleep(EVENT_PROPAGATION_DELAY);
- await sleep(10);
- sinon.assert.calledOnce(putStub);
- sinon.assert.notCalled(putStatusStub);
+ sinon.assert.calledTwice(stubs.update);
+ sinon.assert.calledOnce(stubs.updateStatus);
});
it('should not crash if the resource does not exist upon "mod" volume event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
sinon.stub(volumes, 'get').returns([]);
- oper = await mockedVolumeOperator([], volumes);
- // we have to sleep to give event stream chance to register its handlers
- await sleep(10);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns();
+ });
const newSpec = {
replicaCount: 3,
@@ -759,75 +788,81 @@ module.exports = function () {
eventType: 'mod',
object: volume
});
+ await sleep(EVENT_PROPAGATION_DELAY);
- await sleep(10);
- sinon.assert.notCalled(postStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.notCalled(putStatusStub);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
+ sinon.assert.notCalled(stubs.updateStatus);
});
it('should delete the resource upon "del" volume event', async () => {
- const obj = createVolumeResource(UUID, defaultSpec);
+ let stubs;
+ const volumeResource = createVolumeResource(UUID, defaultSpec);
const registry = new Registry();
const volumes = new Volumes(registry);
sinon.stub(volumes, 'get').returns([]);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(obj);
- // we have to sleep to give event stream chance to register its handlers
- await sleep(10);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ stubs.delete.resolves();
+ });
const volume = new Volume(UUID, registry, defaultSpec);
volumes.emit('volume', {
eventType: 'del',
object: volume
});
+ await sleep(EVENT_PROPAGATION_DELAY);
- await sleep(10);
- sinon.assert.calledOnce(deleteStub);
+ sinon.assert.calledOnce(stubs.delete);
});
it('should not crash if DELETE fails upon "del" volume event', async () => {
- const obj = createVolumeResource(UUID, defaultSpec);
+ let stubs;
+ const volumeResource = createVolumeResource(UUID, defaultSpec);
const registry = new Registry();
const volumes = new Volumes(registry);
sinon.stub(volumes, 'get').returns([]);
- oper = await mockedVolumeOperator([], volumes);
- oper.watcher.injectObject(obj);
- // we have to sleep to give event stream chance to register its handlers
- await sleep(10);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns(volumeResource);
+ stubs.delete.rejects(new Error('delete failed'));
+ });
- deleteStub.rejects(new Error('delete failed'));
const volume = new Volume(UUID, registry, defaultSpec);
volumes.emit('volume', {
eventType: 'del',
object: volume
});
+ await sleep(EVENT_PROPAGATION_DELAY);
- await sleep(10);
- sinon.assert.calledOnce(deleteStub);
+ sinon.assert.calledOnce(stubs.delete);
});
it('should not crash if the resource does not exist upon "del" volume event', async () => {
+ let stubs;
const registry = new Registry();
const volumes = new Volumes(registry);
sinon.stub(volumes, 'get').returns([]);
- oper = await mockedVolumeOperator([], volumes);
- // we have to sleep to give event stream chance to register its handlers
- await sleep(10);
+ oper = await createVolumeOperator(volumes, (arg) => {
+ stubs = arg;
+ stubs.get.returns();
+ stubs.delete.resolves();
+ });
const volume = new Volume(UUID, registry, defaultSpec);
volumes.emit('volume', {
eventType: 'del',
object: volume
});
+ await sleep(EVENT_PROPAGATION_DELAY);
- await sleep(10);
- sinon.assert.notCalled(deleteStub);
- sinon.assert.notCalled(putStub);
- sinon.assert.notCalled(postStub);
+ sinon.assert.notCalled(stubs.delete);
+ sinon.assert.notCalled(stubs.create);
+ sinon.assert.notCalled(stubs.update);
});
});
};
diff --git a/csi/moac/test/watcher_stub.js b/csi/moac/test/watcher_stub.js
index b1ea6bf74..8dbbeeb49 100644
--- a/csi/moac/test/watcher_stub.js
+++ b/csi/moac/test/watcher_stub.js
@@ -1,79 +1,49 @@
-// Fake watcher which simulates the real one.
+// Fake watcher that isolates the watcher from k8s api server using sinon stubs.
'use strict';
-const assert = require('assert');
-const EventEmitter = require('events');
-
-// It can be used instead of real watcher in tests of other classes depending
-// on the watcher.
-class Watcher extends EventEmitter {
- // Construct a watcher with initial set of objects passed in arg.
- constructor (filterCb, objects) {
- super();
- this.filterCb = filterCb;
- this.objects = {};
- for (let i = 0; i < objects.length; i++) {
- this.objects[objects[i].metadata.name] = objects[i];
- }
- }
-
- injectObject (obj) {
- this.objects[obj.metadata.name] = obj;
- }
-
- newObject (obj) {
- this.objects[obj.metadata.name] = obj;
- this.emit('new', this.filterCb(obj));
- }
-
- delObject (name) {
- var obj = this.objects[name];
- assert(obj);
- delete this.objects[name];
- this.emit('del', this.filterCb(obj));
- }
-
- modObject (obj) {
- this.objects[obj.metadata.name] = obj;
- this.emit('mod', this.filterCb(obj));
- }
-
- async start () {
- var self = this;
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- for (const name in self.objects) {
- // real objects coming from GET method also don't have kind and
- // apiVersion attrs so strip these props to mimic the real case.
- delete self.objects[name].kind;
- delete self.objects[name].apiVersion;
- self.emit('new', self.filterCb(self.objects[name]));
- }
- resolve();
- }, 0);
+const sinon = require('sinon');
+
+// stubsCb callback can override default return values of k8s api calls
+function mockCache (cache, stubsCb) {
+ // do not wait for confirming events from k8s
+ cache.eventTimeout = 0;
+
+ // mock k8s api calls
+ cache.createStub = sinon.stub(cache.k8sApi, 'createNamespacedCustomObject');
+ cache.updateStub = sinon.stub(cache.k8sApi, 'replaceNamespacedCustomObject');
+ cache.updateStatusStub = sinon.stub(cache.k8sApi, 'replaceNamespacedCustomObjectStatus');
+ cache.deleteStub = sinon.stub(cache.k8sApi, 'deleteNamespacedCustomObject');
+ cache.getStub = sinon.stub(cache.listWatch, 'get');
+ cache.listStub = sinon.stub(cache.listWatch, 'list');
+ const stubs = {
+ create: cache.createStub,
+ update: cache.updateStub,
+ updateStatus: cache.updateStatusStub,
+ delete: cache.deleteStub,
+ get: cache.getStub,
+ list: cache.listStub
+ };
+ stubs.create.resolves();
+ stubs.update.resolves();
+ stubs.updateStatus.resolves();
+ stubs.delete.resolves();
+ stubs.get.returns();
+ stubs.list.returns([]);
+ if (stubsCb) stubsCb(stubs);
+
+ // convenience function for emitting watcher events
+ stubs.emitKubeEvent = (ev, data) => {
+ cache.listWatch.callbackCache[ev].forEach((cb) => cb(data));
+ };
+
+ // mock the watcher to start even without k8s
+ const startStub = sinon.stub(cache.listWatch, 'start');
+ startStub.callsFake(async () => {
+ stubs.list().forEach((ent) => {
+ stubs.emitKubeEvent('add', ent);
});
- }
-
- async stop () {}
-
- async getRawBypass (name) {
- return this.getRaw(name);
- }
-
- getRaw (name) {
- const obj = this.objects[name];
- if (!obj) {
- return null;
- } else {
- return JSON.parse(JSON.stringify(obj));
- }
- }
-
- list () {
- var self = this;
- return Object.values(this.objects).map((ent) => self.filterCb(ent));
- }
+ });
}
-module.exports = Watcher;
+module.exports = { mockCache };
diff --git a/csi/moac/test/watcher_test.js b/csi/moac/test/watcher_test.js
index 5c58658ea..121881ea3 100644
--- a/csi/moac/test/watcher_test.js
+++ b/csi/moac/test/watcher_test.js
@@ -1,12 +1,34 @@
-// Unit tests for the watcher.
-//
-// We fake the k8s api watch and collection endpoints so that the tests are
-// runable without k8s environment and let us test corner cases which would
-// normally be impossible to test.
+// Tests for the object cache (watcher).
+const _ = require('lodash');
const expect = require('chai').expect;
-const Watcher = require('../watcher');
-const Readable = require('stream').Readable;
+const sinon = require('sinon');
+const sleep = require('sleep-promise');
+const { KubeConfig } = require('client-node-fixed-watcher');
+const { CustomResourceCache } = require('../watcher');
+
+// slightly modified cache tunings not to wait too long when testing things
+const IDLE_TIMEOUT_MS = 500;
+const RESTART_DELAY_MS = 300;
+const EVENT_TIMEOUT_MS = 200;
+const EVENT_DELAY_MS = 100;
+const EYE_BLINK_MS = 30;
+
+const fakeConfig = {
+ clusters: [
+ {
+ name: 'cluster',
+ server: 'foo.company.com'
+ }
+ ],
+ contexts: [
+ {
+ cluster: 'cluster',
+ user: 'user'
+ }
+ ],
+ users: [{ name: 'user' }]
+};
// Create fake k8s object. Example of true k8s object follows:
//
@@ -36,490 +58,460 @@ const Readable = require('stream').Readable;
// ...
// }
// }
-function createObject (name, generation, val) {
+function createApple (name, finalizers, spec) {
return {
- kind: 'mykind',
apiVersion: 'my.group.io/v1alpha1',
- metadata: { name, generation },
- spec: { val }
+ kind: 'apple',
+ metadata: { name, finalizers },
+ spec
};
}
-// Simple filter that produces objects {name, val} from the objects
-// created by the createObject() above and only objects with val > 100
-// pass through the filter.
-function objectFilter (k8sObject) {
- if (k8sObject.kind !== 'mykind') {
- return null;
- }
- if (k8sObject.spec.val > 100) {
- return {
- name: k8sObject.metadata.name,
- val: k8sObject.spec.val
+// Test class
+class Apple {
+ constructor (obj) {
+ this.metadata = {
+ name: obj.metadata.name
};
- } else {
- return null;
- }
-}
-
-// A stub for GET k8s API request returning a collection of k8s objects which
-// were previously set by add() method.
-class GetMock {
- constructor (delay) {
- this.delay = delay;
- this.objects = {};
- this.statusCode = 200;
- }
-
- setStatusCode (code) {
- this.statusCode = code;
- }
-
- add (obj) {
- this.objects[obj.metadata.name] = obj;
- }
-
- remove (name) {
- delete this.objects[name];
- }
-
- reset () {
- this.objects = {};
- }
-
- template () {
- var gMock = this;
- function template (name) {
- return { get: async function () { return gMock.getForce(name); } };
- }
- template.get = function () { return gMock.get(); };
- return template;
- }
-
- async getForce (name) {
- if (this.objects[name]) {
- return { statusCode: this.statusCode, body: this.objects[name] };
- }
- throw Object.assign(
- new Error(`"${name}" not found`),
- { code: 404 }
- );
- }
-
- get () {
- var self = this;
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- resolve({
- statusCode: 200,
- body: { items: Object.values(self.objects) }
- });
- }, self.delay || 0);
- });
- }
-}
-
-// A mock representing k8s watch stream.
-// You can feed arbitrary objects to it and it will pass them to a consumer.
-// Example of k8s watch stream event follows:
-//
-// {
-// "type": "ADDED",
-// "object": {
-// ... (object as shown in GetMock example above)
-// }
-// }
-class StreamMock extends Readable {
- constructor () {
- super({ autoDestroy: true, objectMode: true });
- this.feeds = [];
- this.wantMore = false;
- }
-
- _read (size) {
- while (true) {
- const obj = this.feeds.shift();
- if (obj === undefined) {
- this.wantMore = true;
- break;
- }
- this.push(obj);
+ if (obj.spec === 'invalid') {
+ throw new Error('Invalid object');
}
- }
-
- feed (type, object) {
- this.feeds.push({
- type,
- object
- });
- if (this.wantMore) {
- this.wantMore = false;
- this._read();
- }
- }
-
- end () {
- this.feeds.push(null);
- if (this.wantMore) {
- this.wantMore = false;
- this._read();
- }
- }
-
- getObjectStream () {
- return this;
+ this.spec = obj.spec;
}
}
-// This is for test cases where we need to test disconnected watch stream.
-// In that case, the watcher will create a new instance of watch stream
-// (by calling getObjectStream) and we need to keep track of latest created stream
-// in order to be able to feed data to it etc.
-class StreamMockTracker {
- constructor () {
- this.current = null;
- }
-
- // create a new stream (mimics nodejs k8s client api)
- getObjectStream () {
- const s = new StreamMock();
- this.current = s;
- return s;
- }
-
- // get the most recently created underlaying stream
- latest () {
- return this.current;
- }
+// Create a cache with a listWatch object with fake start method that does
+// nothing instead of connecting to k8s cluster.
+function createMockedCache () {
+ const kc = new KubeConfig();
+ Object.assign(kc, fakeConfig);
+ const watcher = new CustomResourceCache('namespace', 'apple', kc, Apple, {
+ restartDelay: RESTART_DELAY_MS,
+ eventTimeout: EVENT_TIMEOUT_MS,
+ idleTimeout: IDLE_TIMEOUT_MS
+ });
+ // convenience function for generating k8s watcher events
+ watcher.emitKubeEvent = (ev, data) => {
+ watcher.listWatch.callbackCache[ev].forEach((cb) => cb(data));
+ };
+ const startStub = sinon.stub(watcher.listWatch, 'start');
+ startStub.onCall(0).resolves();
+ return [watcher, startStub];
}
module.exports = function () {
- // Basic watcher operations grouped in describe to avoid repeating watcher
- // initialization & tear down for each test case.
- describe('watch events', () => {
- var getMock = new GetMock();
- var streamMock = new StreamMock();
- var watcher;
- var newList = [];
- var modList = [];
- var delList = [];
-
- before(() => {
- watcher = new Watcher('test', getMock.template(), streamMock, objectFilter);
- watcher.on('new', (obj) => newList.push(obj));
- watcher.on('mod', (obj) => modList.push(obj));
- watcher.on('del', (obj) => delList.push(obj));
-
- getMock.add(createObject('valid-object', 1, 123));
- getMock.add(createObject('invalid-object', 1, 99));
- });
-
- after(() => {
- watcher.stop();
- streamMock.end();
+ this.timeout(10000);
+
+ it('should create a cache and block in start until connected', async () => {
+ const kc = new KubeConfig();
+ Object.assign(kc, fakeConfig);
+ const watcher = new CustomResourceCache('namespace', 'apple', kc, Apple, {
+ restartDelay: RESTART_DELAY_MS,
+ eventTimeout: EVENT_TIMEOUT_MS
});
+ const startStub = sinon.stub(watcher.listWatch, 'start');
+ startStub.onCall(0).rejects();
+ startStub.onCall(1).rejects();
+ startStub.onCall(2).resolves();
+ const startTime = new Date();
+ await watcher.start();
+ const delta = new Date() - startTime;
+ sinon.assert.calledThrice(startStub);
+ expect(watcher.isConnected()).to.be.true();
+ expect(delta).to.be.within(2 * RESTART_DELAY_MS, 3 * RESTART_DELAY_MS);
+ watcher.stop();
+ });
- it('should init cache only with objects which pass through the filter', async () => {
- await watcher.start();
+ it('should reconnect watcher if it gets disconnected', async () => {
+ const [watcher, startStub] = createMockedCache();
+ await watcher.start();
+ sinon.assert.calledOnce(startStub);
+ expect(watcher.isConnected()).to.be.true();
+ startStub.onCall(1).rejects(new Error('start failed'));
+ startStub.onCall(2).resolves();
+ watcher.emitKubeEvent('error', new Error('got disconnected'));
+ await sleep(RESTART_DELAY_MS * 1.5);
+ sinon.assert.calledTwice(startStub);
+ expect(watcher.isConnected()).to.be.false();
+ await sleep(RESTART_DELAY_MS);
+ sinon.assert.calledThrice(startStub);
+ expect(watcher.isConnected()).to.be.true();
+ watcher.stop();
+ });
- expect(modList).to.have.lengthOf(0);
- expect(delList).to.have.lengthOf(0);
- expect(newList).to.have.lengthOf(1);
- expect(newList[0].name).to.equal('valid-object');
- expect(newList[0].val).to.equal(123);
+ it('should reset watcher if idle for too long', async () => {
+ const [watcher, startStub] = createMockedCache();
+ await watcher.start();
+ sinon.assert.calledOnce(startStub);
+ expect(watcher.isConnected()).to.be.true();
+ startStub.onCall(1).resolves();
+ await sleep(IDLE_TIMEOUT_MS * 1.5);
+ sinon.assert.calledTwice(startStub);
+ expect(watcher.isConnected()).to.be.true();
+ watcher.stop();
+ });
- const lst = watcher.list();
- expect(lst).to.have.lengthOf(1);
- expect(lst[0]).to.have.all.keys('name', 'val');
- expect(lst[0].name).to.equal('valid-object');
- expect(lst[0].val).to.equal(123);
+ describe('methods', function () {
+ let watcher;
+ let timeout;
- const rawObj = watcher.getRaw('valid-object');
- expect(rawObj).to.deep.equal(createObject('valid-object', 1, 123));
+ beforeEach(async () => {
+ let startStub;
+ timeout = undefined;
+ [watcher, startStub] = createMockedCache();
+ startStub.resolves();
+ await watcher.start();
});
- it('should add object to the cache only if it passes through the filter', (done) => {
- // invalid object should not be added
- streamMock.feed('ADDED', createObject('add-invalid-object', 1, 90));
- // valid object should be added
- streamMock.feed('ADDED', createObject('evented-object', 1, 155));
-
- function check () {
- expect(modList).to.have.lengthOf(0);
- expect(delList).to.have.lengthOf(0);
- expect(newList).to.have.lengthOf(2);
- expect(newList[1].name).to.equal('evented-object');
- expect(newList[1].val).to.equal(155);
- done();
+ afterEach(() => {
+ if (watcher) {
+ watcher.stop();
+ watcher = undefined;
}
-
- // Use a trick to check 'new' event regardless if it has already arrived
- // or will arrive yet.
- if (newList.length > 1) {
- check();
- } else {
- watcher.once('new', () => process.nextTick(check));
+ if (timeout) {
+ clearTimeout(timeout);
}
});
- it('should modify object in the cache if it passes through the filter', (done) => {
- // new object should be added and new event emitted (not the mod event)
- streamMock.feed('MODIFIED', createObject('new-object', 1, 160));
- // object with old generation number should be ignored
- streamMock.feed('MODIFIED', createObject('evented-object', 1, 155));
- // object should be modified
- streamMock.feed('MODIFIED', createObject('evented-object', 2, 156));
- // object should be modified (without gen number)
- streamMock.feed(
- 'MODIFIED',
- createObject('evented-object', undefined, 157)
- );
-
- function check () {
- expect(delList).to.have.lengthOf(0);
- expect(modList).to.have.lengthOf(2);
- expect(modList[0].name).to.equal('evented-object');
- expect(modList[0].val).to.equal(156);
- expect(modList[1].name).to.equal('evented-object');
- expect(modList[1].val).to.equal(157);
- expect(newList).to.have.lengthOf(3);
- expect(newList[2].name).to.equal('new-object');
- expect(newList[2].val).to.equal(160);
- done();
- }
+ function assertReplaceCalledWith (stub, name, obj, attrs) {
+ const newObj = _.cloneDeep(obj);
+ _.merge(newObj, attrs);
+ sinon.assert.calledOnce(stub);
+ sinon.assert.calledWith(stub, 'openebs.io', 'v1alpha1', 'namespace',
+ 'apples', name, newObj);
+ }
- if (modList.length > 0) {
- check();
- } else {
- watcher.once('mod', () => process.nextTick(check));
- }
+ it('should list all objects', () => {
+ const listStub = sinon.stub(watcher.listWatch, 'list');
+ listStub.returns([
+ createApple('name1', [], 'valid'),
+ createApple('name2', [], 'invalid'),
+ createApple('name3', [], 'valid')
+ ]);
+ const objs = watcher.list();
+ expect(objs).to.have.length(2);
+ expect(objs[0].metadata.name).to.equal('name1');
+ expect(objs[1].metadata.name).to.equal('name3');
});
- it('should remove object from the cache if it exists', (done) => {
- streamMock.feed('DELETED', createObject('unknown-object', 1, 160));
- streamMock.feed('DELETED', createObject('evented-object', 2, 156));
-
- function check () {
- expect(newList).to.have.lengthOf(3);
- expect(modList).to.have.lengthOf(2);
- expect(delList).to.have.lengthOf(1);
- expect(delList[0].name).to.equal('evented-object');
- expect(delList[0].val).to.equal(156);
- done();
- }
-
- if (delList.length > 0) {
- check();
- } else {
- watcher.once('del', () => process.nextTick(check));
- }
+ it('should get object by name', () => {
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ getStub.returns(createApple('name1', [], 'valid'));
+ const obj = watcher.get('name1');
+ expect(obj).to.be.an.instanceof(Apple);
+ expect(obj.metadata.name).to.equal('name1');
+ sinon.assert.calledWith(getStub, 'name1');
});
- it('should not crash upon error watch event', () => {
- streamMock.feed('ERROR', createObject('error-object', 1, 160));
+ it('should get undefined if object does not exist', () => {
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ getStub.returns(undefined);
+ const obj = watcher.get('name1');
+ expect(obj).to.be.undefined();
+ sinon.assert.calledWith(getStub, 'name1');
});
- it('should not crash upon unknown watch event', () => {
- streamMock.feed('UNKNOWN', createObject('some-object', 1, 160));
+ it('should create an object and wait for new event', async () => {
+ const createStub = sinon.stub(watcher.k8sApi, 'createNamespacedCustomObject');
+ createStub.resolves();
+ const apple = createApple('name1', [], 'valid');
+ const startTime = new Date();
+ timeout = setTimeout(() => watcher.emitKubeEvent('add', apple), EVENT_DELAY_MS);
+ await watcher.create(apple);
+ const delta = new Date() - startTime;
+ expect(delta).to.be.within(EVENT_DELAY_MS - 2, EVENT_DELAY_MS + EYE_BLINK_MS);
+ sinon.assert.calledOnce(createStub);
});
- it('should bypass the watcher when using getRawBypass', async () => {
- await watcher.start();
-
- getMock.add(createObject('new-object', 1, 123));
-
- var obj = watcher.getRaw('new-object');
- expect(obj).is.null();
-
- obj = await watcher.getRawBypass('new-object');
- expect(obj).is.not.null();
-
- // getRawBypass also adds the newly retrieved object to the watcher cache so we should now see it
- obj = watcher.getRaw('new-object');
- expect(obj).is.not.null();
+ it('should timeout when "add" event does not come after a create', async () => {
+ const createStub = sinon.stub(watcher.k8sApi, 'createNamespacedCustomObject');
+ createStub.resolves();
+ const apple = createApple('name1', [], 'valid');
+ const startTime = new Date();
+ await watcher.create(apple);
+ const delta = new Date() - startTime;
+ expect(delta).to.be.within(EVENT_TIMEOUT_MS, EVENT_TIMEOUT_MS + EYE_BLINK_MS);
+ sinon.assert.calledOnce(createStub);
});
- it('should fail gracefully when using getRawBypass', async () => {
- await watcher.start();
-
- var obj = await watcher.getRawBypass('new-object-2');
- expect(obj).is.null();
+ it('should update object and wait for mod event', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ const newApple = createApple('name1', [], 'also valid');
+ getStub.returns(apple);
+ const startTime = new Date();
+ timeout = setTimeout(() => watcher.emitKubeEvent('update', newApple), EVENT_DELAY_MS);
+ await watcher.update('name1', (orig) => {
+ return createApple(orig.metadata.name, [], 'also valid');
+ });
+ const delta = new Date() - startTime;
+ expect(delta).to.be.within(EVENT_DELAY_MS - 2, EVENT_DELAY_MS + EYE_BLINK_MS);
+ assertReplaceCalledWith(replaceStub, 'name1', apple, {
+ spec: 'also valid'
+ });
+ });
- getMock.add(createObject('new-object-2', 1, 123));
+ it('should not try to update object if it does not exist', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ getStub.returns();
+ await watcher.update('name1', (orig) => {
+ return createApple(orig.metadata.name, [], 'also valid');
+ });
+ sinon.assert.notCalled(replaceStub);
+ });
- getMock.setStatusCode(408);
+ it('should timeout when "update" event does not come after an update', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ const startTime = new Date();
+ await watcher.update('name1', (orig) => {
+ return createApple(orig.metadata.name, [], 'also valid');
+ });
+ const delta = new Date() - startTime;
+ expect(delta).to.be.within(EVENT_TIMEOUT_MS, EVENT_TIMEOUT_MS + EYE_BLINK_MS);
+ sinon.assert.calledOnce(replaceStub);
+ });
- obj = await watcher.getRawBypass('new-object-2');
- expect(obj).is.null();
+ it('should retry update of an object if it fails', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.onCall(0).rejects(new Error('update failed'));
+ replaceStub.onCall(1).resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ await watcher.update('name1', (orig) => {
+ return createApple(orig.metadata.name, [], 'also valid');
+ });
+ sinon.assert.calledTwice(replaceStub);
+ });
- obj = watcher.getRaw('new-object-2');
- expect(obj).is.null();
+ it('should update status of object', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObjectStatus');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ await watcher.updateStatus('name1', (orig) => {
+ return _.assign({}, apple, {
+ status: 'some-state'
+ });
+ });
+ assertReplaceCalledWith(replaceStub, 'name1', apple, {
+ status: 'some-state'
+ });
+ });
- getMock.setStatusCode(200);
+ it('should not try to update status of object if it does not exist', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObjectStatus');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns();
+ await watcher.updateStatus('name1', (orig) => {
+ return _.assign({}, apple, {
+ status: 'some-state'
+ });
+ });
+ sinon.assert.notCalled(replaceStub);
+ });
- obj = await watcher.getRawBypass('new-object-2');
- expect(obj).is.not.null();
+ it('should timeout when "update" event does not come after status update', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObjectStatus');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ const startTime = new Date();
+ await watcher.updateStatus('name1', (orig) => {
+ return _.assign({}, apple, {
+ status: 'some-state'
+ });
+ });
+ const delta = new Date() - startTime;
+ expect(delta).to.be.within(EVENT_TIMEOUT_MS, EVENT_TIMEOUT_MS + EYE_BLINK_MS);
+ sinon.assert.calledOnce(replaceStub);
});
- });
- it('should defer event processing when sync is in progress', async () => {
- var getMock = new GetMock();
- var streamMock = new StreamMock();
- var watcher = new Watcher('test', getMock, streamMock, objectFilter);
- var newCount = 0;
- var modCount = 0;
-
- // Use trick of queueing event with newer generation # for an object which
- // is returned by GET. If event processing is done after GET, then we will
- // see one new and one mod event. If not then we will see only one new
- // event.
- getMock.add(createObject('object', 1, 155));
- streamMock.feed('MODIFIED', createObject('object', 2, 156));
- watcher.on('new', () => newCount++);
- watcher.on('mod', () => modCount++);
+ it('should retry status update of an object if it fails', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObjectStatus');
+ replaceStub.onCall(0).rejects(new Error('update failed'));
+ replaceStub.onCall(1).resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ await watcher.updateStatus('name1', (orig) => {
+ return _.assign({}, apple, {
+ status: 'some-state'
+ });
+ });
+ sinon.assert.calledTwice(replaceStub);
+ });
- await watcher.start();
+ it('should fail if status update fails twice', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObjectStatus');
+ replaceStub.onCall(0).rejects(new Error('update failed first time'));
+ replaceStub.onCall(1).rejects(new Error('update failed second time'));
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ let error;
+ try {
+ await watcher.updateStatus('name1', (orig) => {
+ return _.assign({}, apple, {
+ status: 'some-state'
+ });
+ });
+ } catch (err) {
+ error = err;
+ }
+ expect(error.message).to.equal('Status update of apple "name1" failed: update failed second time');
+ sinon.assert.calledTwice(replaceStub);
+ });
- expect(newCount).to.equal(1);
- expect(modCount).to.equal(1);
+ it('should delete the object and wait for "delete" event', async () => {
+ const deleteStub = sinon.stub(watcher.k8sApi, 'deleteNamespacedCustomObject');
+ deleteStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ const startTime = new Date();
+ timeout = setTimeout(() => watcher.emitKubeEvent('delete', apple), EVENT_DELAY_MS);
+ await watcher.delete('name1');
+ const delta = new Date() - startTime;
+ sinon.assert.calledOnce(deleteStub);
+ sinon.assert.calledWith(deleteStub, 'openebs.io', 'v1alpha1', 'namespace',
+ 'apples', 'name1');
+ expect(delta).to.be.within(EVENT_DELAY_MS - 2, EVENT_DELAY_MS + EYE_BLINK_MS);
+ });
- watcher.stop();
- streamMock.end();
- });
+ it('should timeout when "delete" event does not come after a delete', async () => {
+ const deleteStub = sinon.stub(watcher.k8sApi, 'deleteNamespacedCustomObject');
+ deleteStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ const startTime = new Date();
+ await watcher.delete('name1');
+ const delta = new Date() - startTime;
+ sinon.assert.calledOnce(deleteStub);
+ expect(delta).to.be.within(EVENT_TIMEOUT_MS, EVENT_TIMEOUT_MS + EYE_BLINK_MS);
+ });
- it('should merge old and new objects upon resync', (done) => {
- var getMock = new GetMock();
- var streamMockTracker = new StreamMockTracker();
- var watcher = new Watcher('test', getMock, streamMockTracker, objectFilter);
- var newObjs = [];
- var modObjs = [];
- var delObjs = [];
-
- getMock.add(createObject('object-to-be-retained', 1, 155));
- getMock.add(createObject('object-to-be-modified', 1, 155));
- getMock.add(createObject('object-to-be-deleted', 1, 155));
-
- watcher.on('new', (obj) => newObjs.push(obj));
- watcher.on('mod', (obj) => modObjs.push(obj));
- watcher.on('del', (obj) => delObjs.push(obj));
-
- watcher.start().then(() => {
- expect(newObjs).to.have.lengthOf(3);
- expect(modObjs).to.have.lengthOf(0);
- expect(delObjs).to.have.lengthOf(0);
-
- streamMockTracker
- .latest()
- .feed('MODIFIED', createObject('object-to-be-retained', 2, 156));
- getMock.reset();
- getMock.add(createObject('object-to-be-retained', 2, 156));
- getMock.add(createObject('object-to-be-modified', 2, 156));
- getMock.add(createObject('object-to-be-created', 1, 156));
-
- streamMockTracker.latest().end();
-
- watcher.once('sync', () => {
- expect(newObjs).to.have.lengthOf(4);
- expect(modObjs).to.have.lengthOf(2);
- expect(delObjs).to.have.lengthOf(1);
- expect(newObjs[3].name).to.equal('object-to-be-created');
- expect(modObjs[0].name).to.equal('object-to-be-retained');
- expect(modObjs[1].name).to.equal('object-to-be-modified');
- expect(delObjs[0].name).to.equal('object-to-be-deleted');
+ it('should not try to delete object that does not exist', async () => {
+ const deleteStub = sinon.stub(watcher.k8sApi, 'deleteNamespacedCustomObject');
+ deleteStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns();
+ timeout = setTimeout(() => watcher.emitKubeEvent('delete', apple), EVENT_DELAY_MS);
+ await watcher.delete('name1');
+ sinon.assert.notCalled(deleteStub);
+ });
- watcher.stop();
- streamMockTracker.latest().end();
- done();
+ it('should add finalizer to object without any', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns(apple);
+ const startTime = new Date();
+ timeout = setTimeout(() => watcher.emitKubeEvent('update', apple), EVENT_DELAY_MS);
+ await watcher.addFinalizer('name1', 'test.finalizer.com');
+ const delta = new Date() - startTime;
+ expect(delta).to.be.within(EVENT_DELAY_MS - 2, EVENT_DELAY_MS + EYE_BLINK_MS);
+ assertReplaceCalledWith(replaceStub, 'name1', apple, {
+ metadata: {
+ finalizers: ['test.finalizer.com']
+ }
});
});
- });
- it('should recover when watch fails during the sync', async () => {
- class BrokenStreamMock {
- constructor () {
- this.iter = 0;
- this.current = null;
- }
-
- // We will fail (end) the stream 3x and 4th attempt will succeed
- getObjectStream () {
- const s = new StreamMock();
- this.current = s;
- if (this.iter < 3) {
- s.end();
+ it('should add another finalizer to object', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', ['test.finalizer.com', 'test2.finalizer.com'], 'valid');
+ getStub.returns(apple);
+ const startTime = new Date();
+ timeout = setTimeout(() => watcher.emitKubeEvent('update', apple), EVENT_DELAY_MS);
+ await watcher.addFinalizer('name1', 'new.finalizer.com');
+ const delta = new Date() - startTime;
+ expect(delta).to.be.within(EVENT_DELAY_MS - 2, EVENT_DELAY_MS + EYE_BLINK_MS);
+ assertReplaceCalledWith(replaceStub, 'name1', apple, {
+ metadata: {
+ finalizers: ['new.finalizer.com', 'test.finalizer.com', 'test2.finalizer.com']
}
- this.iter++;
- return s;
- }
-
- // get the most recently created underlaying stream
- latest () {
- return this.current;
- }
- }
-
- var getMock = new GetMock(100);
- var brokenStreamMock = new BrokenStreamMock();
- var watcher = new Watcher('test', getMock, brokenStreamMock, objectFilter);
-
- var start = Date.now();
- await watcher.start();
- var diff = (Date.now() - start) / 1000;
+ });
+ });
- // three retries will accumulate 7 seconds (1, 2 and 4s)
- expect(diff).to.be.at.least(6);
- expect(diff).to.be.at.most(8);
- watcher.stop();
- brokenStreamMock.latest().end();
- }).timeout(10000);
-
- it('should recover when GET fails during the sync', async () => {
- class BrokenGetMock {
- constructor (stream) {
- this.stream = stream;
- this.iter = 0;
- }
+ it('should not add twice the same finalizer', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', ['test.finalizer.com', 'test2.finalizer.com'], 'valid');
+ getStub.returns(apple);
+ timeout = setTimeout(() => watcher.emitKubeEvent('update', apple), EVENT_DELAY_MS);
+ await watcher.addFinalizer('name1', 'test.finalizer.com');
+ sinon.assert.notCalled(replaceStub);
+ });
- get () {
- var self = this;
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- if (self.iter++ < 3) {
- const err = new Error('Not found');
- err.statusCode = 404;
- err.body = {};
- reject(err);
- // TODO: defect in current implementation of watcher is that
- // it waits for end of watch connection even when GET fails
- self.stream.latest().end();
- } else {
- resolve({
- statusCode: 200,
- body: { items: [] }
- });
- }
- }, 0);
- });
- }
- }
+ it('should not add the finalizer if object does not exist', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', [], 'valid');
+ getStub.returns();
+ timeout = setTimeout(() => watcher.emitKubeEvent('update', apple), EVENT_DELAY_MS);
+ await watcher.addFinalizer('name1', 'test.finalizer.com');
+ sinon.assert.notCalled(replaceStub);
+ });
- var streamMockTracker = new StreamMockTracker();
- var brokenGetMock = new BrokenGetMock(streamMockTracker);
- var watcher = new Watcher(
- 'test',
- brokenGetMock,
- streamMockTracker,
- objectFilter
- );
+ it('should remove finalizer from object', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', ['test.finalizer.com', 'test2.finalizer.com'], 'valid');
+ getStub.returns(apple);
+ const startTime = new Date();
+ timeout = setTimeout(() => watcher.emitKubeEvent('update', apple), EVENT_DELAY_MS);
+ await watcher.removeFinalizer('name1', 'test.finalizer.com');
+ const delta = new Date() - startTime;
+ expect(delta).to.be.within(EVENT_DELAY_MS - 2, EVENT_DELAY_MS + EYE_BLINK_MS);
+ sinon.assert.calledOnce(replaceStub);
+ assertReplaceCalledWith(replaceStub, 'name1', apple, {
+ metadata: {
+ finalizers: ['test2.finalizer.com']
+ }
+ });
+ });
- var start = Date.now();
- await watcher.start();
- var diff = (Date.now() - start) / 1000;
+ it('should not try to remove finalizer that does not exist', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', ['test2.finalizer.com'], 'valid');
+ getStub.returns(apple);
+ timeout = setTimeout(() => watcher.emitKubeEvent('update', apple), EVENT_DELAY_MS);
+ await watcher.removeFinalizer('name1', 'test.finalizer.com');
+ sinon.assert.notCalled(replaceStub);
+ });
- // three retries will accumulate 7 seconds (1, 2 and 4s)
- expect(diff).to.be.at.least(6);
- expect(diff).to.be.at.most(8);
- watcher.stop();
- streamMockTracker.latest().end();
- }).timeout(10000);
+ it('should not try to remove finalizer if object does not exist', async () => {
+ const replaceStub = sinon.stub(watcher.k8sApi, 'replaceNamespacedCustomObject');
+ replaceStub.resolves();
+ const getStub = sinon.stub(watcher.listWatch, 'get');
+ const apple = createApple('name1', ['test.finalizer.com'], 'valid');
+ getStub.returns();
+ timeout = setTimeout(() => watcher.emitKubeEvent('update', apple), EVENT_DELAY_MS);
+ await watcher.removeFinalizer('name1', 'test.finalizer.com');
+ sinon.assert.notCalled(replaceStub);
+ });
+ });
};
diff --git a/csi/moac/tsconfig.json b/csi/moac/tsconfig.json
index d90c310b0..4e5658426 100644
--- a/csi/moac/tsconfig.json
+++ b/csi/moac/tsconfig.json
@@ -11,7 +11,7 @@
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
- // "sourceMap": true, /* Generates corresponding '.map' file. */
+ "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
@@ -61,9 +61,12 @@
"resolveJsonModule": true /* allows for importing, extracting types from and generating .json files */
},
"files": [
+ "watcher.ts",
+ "nexus.ts",
+ "node_operator.ts",
"replica.ts",
"pool.ts",
- "nexus.ts",
- "finalizer_helper.ts"
+ "pool_operator.ts",
+ "volume_operator.ts",
]
}
diff --git a/csi/moac/volume_operator.js b/csi/moac/volume_operator.js
deleted file mode 100644
index 8810ebd05..000000000
--- a/csi/moac/volume_operator.js
+++ /dev/null
@@ -1,465 +0,0 @@
-// Volume operator managing volume k8s custom resources.
-//
-// Primary motivation for the resource is to provide information about
-// existing volumes. Other actions and their consequences follow:
-//
-// * destroying the resource implies volume destruction (not advisable)
-// * creating the resource implies volume creation (not advisable)
-// * modification of "preferred nodes" property influences scheduling of new replicas
-// * modification of "required nodes" property moves the volume to different nodes
-// * modification of replica count property changes redundancy of the volume
-//
-// Volume operator stands between k8s custom resource (CR) describing desired
-// state and volume manager reflecting the actual state. It gets new/mod/del
-// events from both, from the world of ideas and from the world of material
-// things. It's task which is not easy, is to restore harmony between them:
-//
-// +---------+ new/mod/del +----------+ new/mod/del +-----------+
-// | Volumes +--------------> Operator <---------------+ Watcher |
-// +------^--+ ++--------++ +---^-------+
-// | | | |
-// | | | |
-// +------------------+ +--------------------+
-// create/modify/destroy create/modify/destroy
-//
-//
-// real object event | CR exists | CR does not exist
-// ------------------------------------------------------------
-// new | -- | create CR
-// mod | modify CR | --
-// del | delete CR | --
-//
-//
-// CR event | volume exists | volume does not exist
-// ---------------------------------------------------------------
-// new | modify volume | create volume
-// mod | modify volume | --
-// del | delete volume | --
-//
-
-'use strict';
-
-const _ = require('lodash');
-const path = require('path');
-const assert = require('assert');
-const fs = require('fs');
-const yaml = require('js-yaml');
-const EventStream = require('./event_stream');
-const log = require('./logger').Logger('volume-operator');
-const Watcher = require('./watcher');
-const Workq = require('./workq');
-
-const crdVolume = yaml.safeLoad(
- fs.readFileSync(path.join(__dirname, '/crds/mayastorvolume.yaml'), 'utf8')
-);
-// lower-case letters uuid pattern
-const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/;
-
-// Volume operator managing volume k8s custom resources.
-class VolumeOperator {
- constructor (namespace) {
- this.namespace = namespace;
- this.k8sClient = null; // k8s client
- this.volumes = null; // Volume manager
- this.eventStream = null; // A stream of node, replica and nexus events.
- this.watcher = null; // volume resource watcher.
- this.createdBySelf = []; // UUIDs of volumes created by the operator itself
- // Events from k8s are serialized so that we don't flood moac by
- // concurrent changes to volumes.
- this.workq = new Workq();
- }
-
- // Create volume CRD if it doesn't exist and augment client object so that CRD
- // can be manipulated as any other standard k8s api object.
- //
- // @param {object} k8sClient Client for k8s api server.
- // @param {object} volumes Volume manager.
- //
- async init (k8sClient, volumes) {
- log.info('Initializing volume operator');
-
- try {
- await k8sClient.apis[
- 'apiextensions.k8s.io'
- ].v1beta1.customresourcedefinitions.post({ body: crdVolume });
- log.info('Created CRD ' + crdVolume.spec.names.kind);
- } catch (err) {
- // API returns a 409 Conflict if CRD already exists.
- if (err.statusCode !== 409) throw err;
- }
- k8sClient.addCustomResourceDefinition(crdVolume);
-
- this.k8sClient = k8sClient;
- this.volumes = volumes;
-
- // Initialize watcher with all callbacks for new/mod/del events
- this.watcher = new Watcher(
- 'volume',
- this.k8sClient.apis['openebs.io'].v1alpha1.namespaces(
- this.namespace
- ).mayastorvolumes,
- this.k8sClient.apis['openebs.io'].v1alpha1.watch.namespaces(
- this.namespace
- ).mayastorvolumes,
- this._filterMayastorVolume
- );
- }
-
- // Normalize k8s mayastor volume resource.
- //
- // @param {object} msv MayaStor volume custom resource.
- // @returns {object} Properties defining a volume.
- //
- _filterMayastorVolume (msv) {
- // We should probably validate the whole record using json scheme or
- // something like that, but for now do just the basic check.
- if (!msv.metadata.name.match(uuidRegexp)) {
- log.warn(
- `Ignoring mayastor volume resource with invalid UUID: ${msv.metadata.name}`
- );
- return null;
- }
- if (!msv.spec.requiredBytes) {
- log.warn('Ignoring mayastor volume resource without requiredBytes');
- return null;
- }
- const props = {
- // spec part
- metadata: { name: msv.metadata.name },
- spec: {
- replicaCount: msv.spec.replicaCount || 1,
- preferredNodes: [].concat(msv.spec.preferredNodes || []).sort(),
- requiredNodes: [].concat(msv.spec.requiredNodes || []).sort(),
- requiredBytes: msv.spec.requiredBytes,
- limitBytes: msv.spec.limitBytes || 0,
- protocol: msv.spec.protocol
- }
- };
- // volatile part
- const st = msv.status;
- if (st) {
- props.status = {
- size: st.size,
- state: st.state,
- node: st.node,
- // sort the replicas according to uri to have deterministic order
- replicas: [].concat(st.replicas || []).sort((a, b) => {
- if (a.uri < b.uri) return -1;
- else if (a.uri > b.uri) return 1;
- else return 0;
- })
- };
- if (st.nexus) {
- props.status.nexus = st.nexus;
- }
- }
-
- return props;
- }
-
- // Start volume operator's watcher loop.
- //
- // NOTE: Not getting the start sequence right can have catastrophic
- // consequence leading to unintended volume destruction and data loss.
- // Therefore it's important not to call this function before volume
- // manager and registry have been started up.
- //
- async start () {
- var self = this;
-
- // install event handlers to follow changes to resources.
- self._bindWatcher(self.watcher);
- await self.watcher.start();
-
- // This will start async processing of volume events.
- self.eventStream = new EventStream({ volumes: self.volumes });
- self.eventStream.on('data', async (ev) => {
- // the only kind of event that comes from the volumes source
- assert(ev.kind === 'volume');
-
- self.workq.push(ev, self._onVolumeEvent.bind(self));
- });
- }
-
- async _onVolumeEvent (ev) {
- var self = this;
- const uuid = ev.object.uuid;
-
- if (ev.eventType === 'new' || ev.eventType === 'mod') {
- const k8sVolume = await self.watcher.getRawBypass(uuid);
- const spec = self._volumeToSpec(ev.object);
- const status = self._volumeToStatus(ev.object);
-
- if (k8sVolume) {
- try {
- await self._updateResource(uuid, k8sVolume, spec);
- } catch (err) {
- log.error(`Failed to update volume resource "${uuid}": ${err}`);
- return;
- }
- } else if (ev.eventType === 'new') {
- try {
- await self._createResource(uuid, spec);
- } catch (err) {
- log.error(`Failed to create volume resource "${uuid}": ${err}`);
- return;
- }
- // Note down that the volume existed so we don't try to create it
- // again when handling watcher new event.
- self.createdBySelf.push(uuid);
- }
- await this._updateStatus(uuid, status);
- } else if (ev.eventType === 'del') {
- await self._deleteResource(uuid);
- } else {
- assert(false);
- }
- }
-
- // Transform volume to spec properties used in k8s volume resource.
- //
- // @param {object} volume Volume object.
- // @returns {object} Spec properties.
- //
- _volumeToSpec (volume) {
- return {
- replicaCount: volume.replicaCount,
- preferredNodes: _.clone(volume.preferredNodes),
- requiredNodes: _.clone(volume.requiredNodes),
- requiredBytes: volume.requiredBytes,
- limitBytes: volume.limitBytes,
- protocol: volume.protocol
- };
- }
-
- // Transform volume to status properties used in k8s volume resource.
- //
- // @param {object} volume Volume object.
- // @returns {object} Status properties.
- //
- _volumeToStatus (volume) {
- const st = {
- size: volume.getSize(),
- state: volume.state,
- reason: '',
- node: volume.getNodeName(),
- replicas: Object.values(volume.replicas).map((r) => {
- return {
- node: r.pool.node.name,
- pool: r.pool.name,
- uri: r.uri,
- offline: r.isOffline()
- };
- })
- };
- if (volume.nexus) {
- st.nexus = {
- deviceUri: volume.nexus.deviceUri || '',
- state: volume.nexus.state,
- children: volume.nexus.children.map((ch) => {
- return {
- uri: ch.uri,
- state: ch.state
- };
- })
- };
- }
- return st;
- }
-
- // Create k8s CRD object.
- //
- // @param {string} uuid ID of the created volume.
- // @param {object} spec New volume spec.
- //
- async _createResource (uuid, spec) {
- log.info(`Creating volume resource "${uuid}"`);
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastorvolumes.post({
- body: {
- apiVersion: 'openebs.io/v1alpha1',
- kind: 'MayastorVolume',
- metadata: {
- name: uuid,
- namespace: this.namespace
- },
- spec
- }
- });
- }
-
- // Update properties of k8s CRD object or create it if it does not exist.
- //
- // @param {string} uuid ID of the updated volume.
- // @param {object} k8sVolume Existing k8s resource object.
- // @param {object} spec New volume spec.
- //
- async _updateResource (uuid, k8sVolume, spec) {
- // Update object only if it has really changed
- if (!_.isEqual(k8sVolume.spec, spec)) {
- log.info(`Updating spec of volume resource "${uuid}"`);
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastorvolumes(uuid)
- .put({
- body: {
- apiVersion: 'openebs.io/v1alpha1',
- kind: 'MayastorVolume',
- metadata: k8sVolume.metadata,
- spec: _.assign(k8sVolume.spec, spec)
- }
- });
- }
- }
-
- // Update state and reason of the resource.
- //
- // NOTE: This method does not throw if the operation fails as there is nothing
- // we can do if it fails. Though we log an error message in such a case.
- //
- // @param {string} uuid UUID of the resource.
- // @param {object} status Status properties.
- //
- async _updateStatus (uuid, status) {
- var k8sVolume = await this.watcher.getRawBypass(uuid);
- if (!k8sVolume) {
- log.warn(
- `Wanted to update state of volume resource "${uuid}" that disappeared`
- );
- return;
- }
- if (!k8sVolume.status) {
- k8sVolume.status = {};
- }
- if (_.isEqual(k8sVolume.status, status)) {
- // avoid unnecessary status updates
- return;
- }
- log.debug(`Updating status of volume resource "${uuid}"`);
- _.assign(k8sVolume.status, status);
- try {
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastorvolumes(uuid)
- .status.put({ body: k8sVolume });
- } catch (err) {
- log.error(`Failed to update status of volume resource "${uuid}": ${err}`);
- }
- }
-
- // Delete volume resource with specified uuid.
- //
- // @param {string} uuid UUID of the volume resource to delete.
- //
- async _deleteResource (uuid) {
- var k8sVolume = await this.watcher.getRawBypass(uuid);
- if (k8sVolume) {
- log.info(`Deleting volume resource "${uuid}"`);
- try {
- await this.k8sClient.apis['openebs.io'].v1alpha1
- .namespaces(this.namespace)
- .mayastorvolumes(uuid)
- .delete();
- } catch (err) {
- log.error(`Failed to delete volume resource "${uuid}": ${err}`);
- }
- }
- }
-
- // Stop listening for watcher and node events and reset the cache
- async stop () {
- this.watcher.removeAllListeners();
- await this.watcher.stop();
- this.eventStream.destroy();
- this.eventStream = null;
- }
-
- // Bind watcher's new/mod/del events to volume operator's callbacks.
- //
- // @param {object} watcher k8s volume resource watcher.
- //
- _bindWatcher (watcher) {
- var self = this;
- watcher.on('new', (obj) => {
- self.workq.push(obj, self._importVolume.bind(self));
- });
- watcher.on('mod', (obj) => {
- self.workq.push(obj, self._modifyVolume.bind(self));
- });
- watcher.on('del', (obj) => {
- self.workq.push(obj.metadata.name, self._destroyVolume.bind(self));
- });
- }
-
- // When moac restarts the volume manager does not know which volumes exist.
- // We need to import volumes based on the k8s resources.
- //
- // @param {object} resource Volume resource properties.
- //
- async _importVolume (resource) {
- const uuid = resource.metadata.name;
- const createdIdx = this.createdBySelf.indexOf(uuid);
- if (createdIdx >= 0) {
- // don't react to self
- this.createdBySelf.splice(createdIdx, 1);
- return;
- }
-
- log.debug(`Importing volume "${uuid}" in response to "new" resource event`);
- try {
- await this.volumes.importVolume(uuid, resource.spec, resource.status);
- } catch (err) {
- log.error(
- `Failed to import volume "${uuid}" based on new resource: ${err}`
- );
- await this._updateStatus(uuid, {
- state: 'pending',
- reason: err.toString()
- });
- }
- }
-
- // Modify volume according to the specification.
- //
- // @param {object} resource Volume resource properties.
- //
- async _modifyVolume (resource) {
- const uuid = resource.metadata.name;
- const volume = this.volumes.get(uuid);
-
- if (!volume) {
- log.warn(
- `Volume resource "${uuid}" was modified but the volume does not exist`
- );
- return;
- }
- try {
- if (volume.update(resource.spec)) {
- log.debug(
- `Updating volume "${uuid}" in response to "mod" resource event`
- );
- volume.fsa();
- }
- } catch (err) {
- log.error(`Failed to update volume "${uuid}" based on resource: ${err}`);
- }
- }
-
- // Remove the volume from internal state and if it exists destroy it.
- //
- // @param {string} uuid ID of the volume to destroy.
- //
- async _destroyVolume (uuid) {
- log.debug(
- `Destroying volume "${uuid}" in response to "del" resource event`
- );
- try {
- await this.volumes.destroyVolume(uuid);
- } catch (err) {
- log.error(`Failed to destroy volume "${uuid}": ${err}`);
- }
- }
-}
-
-module.exports = VolumeOperator;
diff --git a/csi/moac/volume_operator.ts b/csi/moac/volume_operator.ts
new file mode 100644
index 000000000..7d9fb6cf3
--- /dev/null
+++ b/csi/moac/volume_operator.ts
@@ -0,0 +1,544 @@
+// Volume operator managing volume k8s custom resources.
+//
+// Primary motivation for the resource is to provide information about
+// existing volumes. Other actions and their consequences follow:
+//
+// * destroying the resource implies volume destruction (not advisable)
+// * creating the resource implies volume import (not advisable)
+// * modification of "preferred nodes" property influences scheduling of new replicas
+// * modification of "required nodes" property moves the volume to different nodes
+// * modification of replica count property changes redundancy of the volume
+//
+// Volume operator stands between k8s custom resource (CR) describing desired
+// state and volume manager reflecting the actual state. It gets new/mod/del
+// events from both, from the world of ideas and from the world of material
+// things. It's task which is not easy, is to restore harmony between them:
+//
+// +---------+ new/mod/del +----------+ new/mod/del +-----------+
+// | Volumes +--------------> Operator <---------------+ Watcher |
+// +------^--+ ++--------++ +---^-------+
+// | | | |
+// | | | |
+// +------------------+ +--------------------+
+// create/modify/destroy create/modify/destroy
+//
+//
+// real object event | CR exists | CR does not exist
+// ------------------------------------------------------------
+// new | -- | create CR
+// mod | modify CR | --
+// del | delete CR | --
+//
+//
+// CR event | volume exists | volume does not exist
+// ---------------------------------------------------------------
+// new | modify volume | create volume
+// mod | modify volume | --
+// del | delete volume | --
+//
+
+const yaml = require('js-yaml');
+const EventStream = require('./event_stream');
+const log = require('./logger').Logger('volume-operator');
+const Workq = require('./workq');
+
+import assert from 'assert';
+import * as fs from 'fs';
+import * as _ from 'lodash';
+import * as path from 'path';
+import {
+ ApiextensionsV1Api,
+ KubeConfig,
+} from 'client-node-fixed-watcher';
+import {
+ CustomResource,
+ CustomResourceCache,
+ CustomResourceMeta,
+} from './watcher';
+
+const RESOURCE_NAME: string = 'mayastorvolume';
+const crdVolume = yaml.safeLoad(
+ fs.readFileSync(path.join(__dirname, '/crds/mayastorvolume.yaml'), 'utf8')
+);
+// lower-case letters uuid pattern
+const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/;
+
+// Protocol used to export nexus (volume)
+enum Protocol {
+ Unknown = 'unknown',
+ Nbd = 'nbd',
+ Iscsi = 'iscsi',
+ Nvmf = 'nvmf',
+}
+
+function protocolFromString(val: string): Protocol {
+ if (val == Protocol.Nbd) {
+ return Protocol.Nbd;
+ } else if (val == Protocol.Iscsi) {
+ return Protocol.Iscsi;
+ } else if (val == Protocol.Nvmf) {
+ return Protocol.Nvmf;
+ } else {
+ return Protocol.Unknown;
+ }
+}
+
+// State of the volume
+enum State {
+ Unknown = 'unknown',
+ Healthy = 'healthy',
+ Degraded = 'degraded',
+ Faulted = 'faulted',
+ Pending = 'pending',
+ Offline = 'offline',
+ Error = 'error',
+}
+
+function stateFromString(val: string): State {
+ if (val == State.Healthy) {
+ return State.Healthy;
+ } else if (val == State.Degraded) {
+ return State.Degraded;
+ } else if (val == State.Faulted) {
+ return State.Faulted;
+ } else if (val == State.Pending) {
+ return State.Pending;
+ } else if (val == State.Offline) {
+ return State.Offline;
+ } else if (val == State.Error) {
+ return State.Error;
+ } else {
+ return State.Unknown;
+ }
+}
+
+// Spec part in volume resource
+type VolumeSpec = {
+ replicaCount: number,
+ preferredNodes: string[],
+ requiredNodes: string[],
+ requiredBytes: number,
+ limitBytes: number,
+ protocol: Protocol,
+};
+
+// Optional status part in volume resource
+type VolumeStatus = {
+ size: number,
+ state: State,
+ reason?: string,
+ node: string,
+ replicas: {
+ node: string,
+ pool: string,
+ uri: string,
+ offline: boolean,
+ }[],
+ nexus?: {
+ deviceUri?: string,
+ state: string,
+ children: {
+ uri: string,
+ state: string,
+ }[]
+ }
+};
+
+// Object defines properties of node resource.
+export class VolumeResource extends CustomResource {
+ apiVersion?: string;
+ kind?: string;
+ metadata: CustomResourceMeta;
+ spec: VolumeSpec;
+ status?: VolumeStatus;
+
+ constructor(cr: CustomResource) {
+ super();
+ this.apiVersion = cr.apiVersion;
+ this.kind = cr.kind;
+ if (cr.metadata?.name === undefined) {
+ throw new Error('Missing name attribute');
+ }
+ this.metadata = cr.metadata;
+ if (!cr.metadata.name.match(uuidRegexp)) {
+ throw new Error(`Invalid UUID`);
+ }
+ let spec = cr.spec as any;
+ if (spec === undefined) {
+ throw new Error('Missing spec section');
+ }
+ if (!spec.requiredBytes) {
+ throw new Error('Missing requiredBytes');
+ }
+ this.spec = {
+ replicaCount: spec.replicaCount || 1,
+ preferredNodes: [].concat(spec.preferredNodes || []).sort(),
+ requiredNodes: [].concat(spec.requiredNodes || []).sort(),
+ requiredBytes: spec.requiredBytes,
+ limitBytes: spec.limitBytes || 0,
+ protocol: protocolFromString(spec.protocol)
+ };
+ let status = cr.status as any;
+ if (status !== undefined) {
+ this.status = {
+ size: status.size || 0,
+ state: stateFromString(status.state),
+ node: status.node,
+ // sort the replicas according to uri to have deterministic order
+ replicas: [].concat(status.replicas || []).sort((a: any, b: any) => {
+ if (a.uri < b.uri) return -1;
+ else if (a.uri > b.uri) return 1;
+ else return 0;
+ }),
+ }
+ if (status.nexus) {
+ this.status.nexus = status.nexus;
+ }
+ }
+ }
+
+ getUuid(): string {
+ let uuid = this.metadata.name;
+ if (uuid === undefined) {
+ throw new Error('Volume resource without UUID');
+ } else {
+ return uuid;
+ }
+ }
+}
+
+// Volume operator managing volume k8s custom resources.
+export class VolumeOperator {
+ namespace: string;
+ volumes: any; // Volume manager
+ eventStream: any; // A stream of node, replica and nexus events.
+ watcher: CustomResourceCache; // volume resource watcher.
+ workq: any; // Events from k8s are serialized so that we don't flood moac by
+ // concurrent changes to volumes.
+
+ // Create volume operator object.
+ //
+ // @param namespace Namespace the operator should operate on.
+ // @param kubeConfig KubeConfig.
+ // @param volumes Volume manager.
+ // @param [idleTimeout] Timeout for restarting watcher connection when idle.
+ constructor (
+ namespace: string,
+ kubeConfig: KubeConfig,
+ volumes: any,
+ idleTimeout: number | undefined,
+ ) {
+ this.namespace = namespace;
+ this.volumes = volumes;
+ this.eventStream = null;
+ this.workq = new Workq();
+ this.watcher = new CustomResourceCache(
+ this.namespace,
+ RESOURCE_NAME,
+ kubeConfig,
+ VolumeResource,
+ { idleTimeout }
+ );
+ }
+
+ // Create volume CRD if it doesn't exist.
+ //
+ // @param kubeConfig KubeConfig.
+ async init (kubeConfig: KubeConfig) {
+ log.info('Initializing volume operator');
+ let k8sExtApi = kubeConfig.makeApiClient(ApiextensionsV1Api);
+ try {
+ await k8sExtApi.createCustomResourceDefinition(crdVolume);
+ log.info(`Created CRD ${RESOURCE_NAME}`);
+ } catch (err) {
+ // API returns a 409 Conflict if CRD already exists.
+ if (err.statusCode !== 409) throw err;
+ }
+ }
+
+ // Start volume operator's watcher loop.
+ //
+ // NOTE: Not getting the start sequence right can have catastrophic
+ // consequence leading to unintended volume destruction and data loss.
+ //
+ async start () {
+ var self = this;
+
+ // install event handlers to follow changes to resources.
+ this._bindWatcher(this.watcher);
+ await this.watcher.start();
+
+ // This will start async processing of volume events.
+ this.eventStream = new EventStream({ volumes: this.volumes });
+ this.eventStream.on('data', async (ev: any) => {
+ // the only kind of event that comes from the volumes source
+ assert(ev.kind === 'volume');
+ self.workq.push(ev, self._onVolumeEvent.bind(self));
+ });
+ }
+
+ async _onVolumeEvent (ev: any) {
+ const uuid = ev.object.uuid;
+
+ if (ev.eventType === 'new' || ev.eventType === 'mod') {
+ const origObj = this.watcher.get(uuid);
+ const spec = {
+ replicaCount: ev.object.replicaCount,
+ preferredNodes: _.clone(ev.object.preferredNodes),
+ requiredNodes: _.clone(ev.object.requiredNodes),
+ requiredBytes: ev.object.requiredBytes,
+ limitBytes: ev.object.limitBytes,
+ protocol: protocolFromString(ev.object.protocol)
+ };
+ const status = this._volumeToStatus(ev.object);
+
+ if (origObj !== undefined) {
+ await this._updateSpec(uuid, origObj, spec);
+ } else if (ev.eventType === 'new') {
+ try {
+ await this._createResource(uuid, spec);
+ } catch (err) {
+ log.error(`Failed to create volume resource "${uuid}": ${err}`);
+ return;
+ }
+ }
+ await this._updateStatus(uuid, status);
+ } else if (ev.eventType === 'del') {
+ await this._deleteResource(uuid);
+ } else {
+ assert(false);
+ }
+ }
+
+ // Transform volume to status properties used in k8s volume resource.
+ //
+ // @param volume Volume object.
+ // @returns Status properties.
+ //
+ _volumeToStatus (volume: any): VolumeStatus {
+ const st: VolumeStatus = {
+ size: volume.getSize(),
+ state: stateFromString(volume.state),
+ node: volume.getNodeName(),
+ replicas: Object.values(volume.replicas).map((r: any) => {
+ return {
+ node: r.pool.node.name,
+ pool: r.pool.name,
+ uri: r.uri,
+ offline: r.isOffline()
+ };
+ })
+ };
+ if (volume.nexus) {
+ st.nexus = {
+ deviceUri: volume.nexus.deviceUri || '',
+ state: volume.nexus.state,
+ children: volume.nexus.children.map((ch: any) => {
+ return {
+ uri: ch.uri,
+ state: ch.state
+ };
+ })
+ };
+ }
+ return st;
+ }
+
+ // Create k8s CRD object.
+ //
+ // @param uuid ID of the created volume.
+ // @param spec New volume spec.
+ //
+ async _createResource (uuid: string, spec: VolumeSpec) {
+ await this.watcher.create({
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorVolume',
+ metadata: {
+ name: uuid,
+ namespace: this.namespace
+ },
+ spec
+ });
+ }
+
+ // Update properties of k8s CRD object or create it if it does not exist.
+ //
+ // @param uuid ID of the updated volume.
+ // @param origObj Existing k8s resource object.
+ // @param spec New volume spec.
+ //
+ async _updateSpec (uuid: string, origObj: VolumeResource, spec: VolumeSpec) {
+ try {
+ await this.watcher.update(uuid, (orig: VolumeResource) => {
+ // Update object only if it has really changed
+ if (_.isEqual(origObj.spec, spec)) {
+ return;
+ }
+ log.info(`Updating spec of volume resource "${uuid}"`);
+ return {
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorVolume',
+ metadata: orig.metadata,
+ spec,
+ };
+ });
+ } catch (err) {
+ log.error(`Failed to update volume resource "${uuid}": ${err}`);
+ return;
+ }
+ }
+
+ // Update status of the volume based on real data obtained from storage node.
+ //
+ // @param uuid UUID of the resource.
+ // @param status Status properties.
+ //
+ async _updateStatus (uuid: string, status: VolumeStatus) {
+ try {
+ await this.watcher.updateStatus(uuid, (orig: VolumeResource) => {
+ if (_.isEqual(orig.status, status)) {
+ // avoid unnecessary status updates
+ return;
+ }
+ log.debug(`Updating status of volume resource "${uuid}"`);
+ // merge old and new properties
+ return {
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorNode',
+ metadata: orig.metadata,
+ spec: orig.spec,
+ status,
+ };
+ });
+ } catch (err) {
+ log.error(`Failed to update status of volume resource "${uuid}": ${err}`);
+ }
+ }
+
+ // Set state and reason not touching the other status fields.
+ async _updateState (uuid: string, state: State, reason: string) {
+ try {
+ await this.watcher.updateStatus(uuid, (orig: VolumeResource) => {
+ if (orig.status?.state === state && orig.status?.reason === reason) {
+ // avoid unnecessary status updates
+ return;
+ }
+ log.debug(`Updating state of volume resource "${uuid}"`);
+ // merge old and new properties
+ let newStatus = _.assign({}, orig.status, { state, reason });
+ return {
+ apiVersion: 'openebs.io/v1alpha1',
+ kind: 'MayastorNode',
+ metadata: orig.metadata,
+ spec: orig.spec,
+ status: newStatus,
+ };
+ });
+ } catch (err) {
+ log.error(`Failed to update status of volume resource "${uuid}": ${err}`);
+ }
+ }
+
+ // Delete volume resource with specified uuid.
+ //
+ // @param uuid UUID of the volume resource to delete.
+ //
+ async _deleteResource (uuid: string) {
+ try {
+ log.info(`Deleting volume resource "${uuid}"`);
+ await this.watcher.delete(uuid);
+ } catch (err) {
+ log.error(`Failed to delete volume resource "${uuid}": ${err}`);
+ }
+ }
+
+ // Stop listening for watcher and node events and reset the cache
+ async stop () {
+ this.watcher.stop();
+ this.watcher.removeAllListeners();
+ if (this.eventStream) {
+ this.eventStream.destroy();
+ this.eventStream = null;
+ }
+ }
+
+ // Bind watcher's new/mod/del events to volume operator's callbacks.
+ //
+ // @param watcher k8s volume resource cache.
+ //
+ _bindWatcher (watcher: CustomResourceCache) {
+ watcher.on('new', (obj: VolumeResource) => {
+ this.workq.push(obj, this._importVolume.bind(this));
+ });
+ watcher.on('mod', (obj: VolumeResource) => {
+ this.workq.push(obj, this._modifyVolume.bind(this));
+ });
+ watcher.on('del', (obj: VolumeResource) => {
+ // most likely it was not user but us (the operator) who deleted
+ // the resource. So check if it really exists first.
+ if (this.volumes.get(obj.metadata.name)) {
+ this.workq.push(obj.metadata.name, this._destroyVolume.bind(this));
+ }
+ });
+ }
+
+ // When moac restarts the volume manager does not know which volumes exist.
+ // We need to import volumes based on the k8s resources.
+ //
+ // @param resource Volume resource properties.
+ //
+ async _importVolume (resource: VolumeResource) {
+ const uuid = resource.getUuid();
+
+ log.debug(`Importing volume "${uuid}" in response to "new" resource event`);
+ try {
+ await this.volumes.importVolume(uuid, resource.spec, resource.status);
+ } catch (err) {
+ log.error(
+ `Failed to import volume "${uuid}" based on new resource: ${err}`
+ );
+ await this._updateState(uuid, State.Error, err.toString());
+ }
+ }
+
+ // Modify volume according to the specification.
+ //
+ // @param resource Volume resource properties.
+ //
+ async _modifyVolume (resource: VolumeResource) {
+ const uuid = resource.getUuid();
+ const volume = this.volumes.get(uuid);
+
+ if (!volume) {
+ log.warn(
+ `Volume resource "${uuid}" was modified but the volume does not exist`
+ );
+ return;
+ }
+ try {
+ if (volume.update(resource.spec)) {
+ log.debug(
+ `Updating volume "${uuid}" in response to "mod" resource event`
+ );
+ volume.fsa();
+ }
+ } catch (err) {
+ log.error(`Failed to update volume "${uuid}" based on resource: ${err}`);
+ }
+ }
+
+ // Remove the volume from internal state and if it exists destroy it.
+ //
+ // @param uuid ID of the volume to destroy.
+ //
+ async _destroyVolume (uuid: string) {
+ log.debug(
+ `Destroying volume "${uuid}" in response to "del" resource event`
+ );
+ try {
+ await this.volumes.destroyVolume(uuid);
+ } catch (err) {
+ log.error(`Failed to destroy volume "${uuid}": ${err}`);
+ }
+ }
+}
diff --git a/csi/moac/watcher.js b/csi/moac/watcher.js
deleted file mode 100644
index 869c268b6..000000000
--- a/csi/moac/watcher.js
+++ /dev/null
@@ -1,318 +0,0 @@
-'use strict';
-
-const assert = require('assert');
-const EventEmitter = require('events');
-const log = require('./logger').Logger('watcher');
-
-// in case of permanent k8s api server failure we retry with max interval
-// of this # of seconds
-const MAX_RECONNECT_DELAY = 30;
-
-// This is a classic operator loop design as seen in i.e. operator-sdk (golang)
-// to watch a k8s resource. We combine http GET with watcher events to do
-// it in an efficient way. First we do GET to populate the cache and then
-// maintain it using watch events. When the watch connection is closed by
-// the server (happens every minute or so), we do GET and continue watch again.
-//
-// It is a general implementation of watcher which can be used for any resource
-// operator. The operator should subscribe to "new", "mod" and "del" events
-// which all pass object parameter and are triggered when a resource is
-// added, modified or deleted.
-//
-class Watcher extends EventEmitter {
- // Construct a watcher for resource.
- // name: name of the watched resource
- // getEp: k8s api endpoint with .get() method to get the objects
- // streamEp: k8s api endpoint with .getObjectStream() method to obtain
- // stream of watch events
- // filterCb: converts k8s object to representation understood by the
- // operator. Or returns null if object should be ignored.
- constructor (name, getEp, streamEp, filterCb) {
- super();
- this.name = name;
- this.getEp = getEp;
- this.streamEp = streamEp;
- this.filterCb = filterCb;
- this.objects = null; // the cache of objects being watched
- this.noRestart = false; // do not renew watcher connection
- this.startResolve = null; // start promise in case of delayed start due
- // to an error
- this.objectStream = null; // non-null if watch connection is active
- this.getInProg = false; // true if GET objects query is in progress
- this.reconnectDelay = 0; // Exponential backoff in case of api server
- // failures (in secs)
- this.pendingEvents = null; // watch events while sync is in progress
- // (if not null -> GET is in progress)
- }
-
- // Start asynchronously the watcher
- async start () {
- var self = this;
- self.objectStream = await self.streamEp.getObjectStream();
-
- // TODO: missing upper bound on exponential backoff
- self.reconnectDelay = Math.min(
- Math.max(2 * self.reconnectDelay, 1),
- MAX_RECONNECT_DELAY
- );
- self.pendingEvents = [];
- assert(!self.getInProg);
- self.getInProg = true;
- // start the stream of events before GET query so that we don't miss any
- // event while performing the GET.
- self.objectStream.on('data', (ev) => {
- log.trace(
- `Event ${ev.type} in ${self.name} watcher: ${JSON.stringify(ev.object)}`
- );
-
- // if update of the node list is in progress, queue the event for later
- if (self.pendingEvents != null) {
- log.trace(`Event deferred until ${self.name} watcher is synced`);
- self.pendingEvents.push(ev);
- return;
- }
-
- self._processEvent(ev);
- });
-
- self.objectStream.on('error', (err) => {
- log.error(`stream error in ${self.name} watcher: ${err}`);
- });
-
- // k8s api server disconnects watcher after a timeout. If that happens
- // reconnect and start again.
- self.objectStream.once('end', () => {
- self.objectStream = null;
- if (self.getInProg) {
- // if watcher disconnected before we finished syncing, we have
- // to wait for the GET request to finish and then start over
- log.error(`${self.name} watch stream closed before the sync completed`);
- } else {
- // reconnect and start watching again
- log.debug(`${self.name} watch stream disconnected`);
- }
- self.scheduleRestart();
- });
-
- var items;
- try {
- const res = await self.getEp.get();
- items = res.body.items;
- } catch (err) {
- log.error(
- `Failed to get list of ${self.name} objects: HTTP ${err.statusCode}`
- );
- self.getInProg = false;
- self.scheduleRestart();
- return self.delayedStart();
- }
-
- // if watcher did end before we retrieved list of objects then start over
- self.getInProg = false;
- if (!self.objectStream) {
- self.scheduleRestart();
- return self.delayedStart();
- }
-
- log.trace(`List of watched ${self.name} objects: ${JSON.stringify(items)}`);
-
- // filter the obtained objects
- var objects = {};
- for (let i = 0; i < items.length; i++) {
- const obj = this.filterCb(items[i]);
- if (obj != null) {
- objects[items[i].metadata.name] = {
- object: obj,
- k8sObject: items[i]
- };
- }
- }
-
- const origObjects = self.objects;
- self.objects = {};
-
- if (origObjects == null) {
- // the first time all objects appear to be new
- for (const name in objects) {
- self.objects[name] = objects[name].k8sObject;
- self.emit('new', objects[name].object);
- }
- } else {
- // Merge old node list with the new node list
- // First delete objects which no longer exist
- for (const name in origObjects) {
- if (!(name in objects)) {
- self.emit('del', self.filterCb(origObjects[name]));
- }
- }
- // Second detect new objects and modified objects
- for (const name in objects) {
- const k8sObj = objects[name].k8sObject;
- const obj = objects[name].object;
- const origObj = origObjects[name];
-
- self.objects[name] = k8sObj;
-
- if (origObj) {
- const generation = k8sObj.metadata.generation;
- // Some objects don't have generation #
- if (!generation || generation > origObj.metadata.generation) {
- self.emit('mod', obj);
- }
- } else {
- self.emit('new', obj);
- }
- }
- }
-
- var ev;
- while ((ev = self.pendingEvents.pop())) {
- self._processEvent(ev);
- }
- self.pendingEvents = null;
- self.reconnectDelay = 0;
-
- log.info(`${self.name} watcher sync completed`);
-
- // if the start was delayed, then resolve the promise now
- if (self.startResolve) {
- self.startResolve();
- self.startResolve = null;
- }
-
- // this event is for test cases
- self.emit('sync');
- }
-
- // Stop does not mean stopping watcher immediately, but rather not restarting
- // it again when watcher connection is closed.
- // TODO: find out how to reset the watcher connection
- async stop () {
- this.noRestart = true;
- }
-
- // Return k8s object(s) from the cache or null if it does not exist.
- getRaw (name) {
- var obj = this.objects[name];
- if (!obj) {
- return null;
- } else {
- return JSON.parse(JSON.stringify(obj));
- }
- }
-
- // Fetches the latest object(s) from k8s and updates the cache; then it
- // returns the k8s object(s) from the cache or null if it does not exist.
- async getRawBypass (name) {
- var getObj = null;
-
- try {
- getObj = await this.getEp(name).get();
- } catch (err) {
- if (err.code !== 404) {
- log.error(`Failed to fetch latest "${name}" from k8s, error: "${err}". Will only use the cached values instead.`);
- }
- }
-
- if (getObj) {
- if (getObj.statusCode === 200) {
- const k8sObj = getObj.body;
- const cachedObj = this.objects[name];
-
- if (!cachedObj) {
- // we still haven't processed the "ADDED" event so add it now
- this._processEvent({
- type: 'ADDED',
- object: k8sObj
- });
- } else if (!k8sObj.metadata.generation || cachedObj.metadata.generation < k8sObj.metadata.generation) {
- // the object already exists so modify it
- this._processEvent({
- type: 'MODIFIED',
- object: k8sObj
- });
- }
- } else {
- const code = getObj.statusCode;
- log.error(`Failed to fetch latest "${name}" from k8s, code: "${code}". Will only use the cached values instead.`);
- }
- }
-
- return this.getRaw(name);
- }
-
- // Return the collection of objects
- list () {
- return Object.values(this.objects).map((ent) => this.filterCb(ent));
- }
-
- delayedStart () {
- var self = this;
-
- if (self.startResolve) {
- return self.startResolve;
- } else {
- return new Promise((resolve, reject) => {
- self.startResolve = resolve;
- });
- }
- }
-
- // Restart the watching process after a timeout
- scheduleRestart () {
- // We cannot restart while either watcher connection or GET query is still
- // in progress. We will get called again when either of them terminates.
- // TODO: How to terminate the watcher connection?
- // Now we simply rely on server to close the conn after timeout
- if (!this.objectStream && !this.getInProg) {
- if (!this.noRestart) {
- setTimeout(this.start.bind(this), 1000 * this.reconnectDelay);
- }
- }
- }
-
- // Invoked when there is a watch event (a resource has changed).
- _processEvent (ev) {
- const k8sObj = ev.object;
- const name = k8sObj.metadata.name;
- const generation = k8sObj.metadata.generation;
- const type = ev.type;
-
- const obj = this.filterCb(k8sObj);
- if (obj == null) {
- return; // not interested in this object
- }
- const oldObj = this.objects[name];
-
- if (type === 'ADDED' || type === 'MODIFIED') {
- this.objects[name] = k8sObj;
- if (!oldObj) {
- // it is a new object with no previous history
- this.emit('new', obj);
- // Some objects don't have generation #
- } else if (!generation || oldObj.metadata.generation < generation) {
- // we assume that if generation # remained the same => no change
- // TODO: add 64-bit integer overflow protection
- this.emit('mod', obj);
- } else if (oldObj.metadata.generation === generation) {
- log.trace(`Status of ${this.name} object changed`);
- } else {
- log.warn(`Ignoring stale ${this.name} object event`);
- }
-
- // TODO: subtle race condition when delete event is related to object which
- // existed before we populated the cache..
- } else if (type === 'DELETED') {
- if (oldObj) {
- delete this.objects[name];
- this.emit('del', obj);
- }
- } else if (type === 'ERROR') {
- log.error(`Error event in ${this.name} watcher: ${JSON.stringify(ev)}`);
- } else {
- log.error(`Unknown event in ${this.name} watcher: ${JSON.stringify(ev)}`);
- }
- }
-}
-
-module.exports = Watcher;
diff --git a/csi/moac/watcher.ts b/csi/moac/watcher.ts
new file mode 100644
index 000000000..b13b54ecf
--- /dev/null
+++ b/csi/moac/watcher.ts
@@ -0,0 +1,555 @@
+// Implementation of a cache for arbitrary k8s custom resource in openebs.io
+// api with v1alpha1 version.
+
+import * as _ from 'lodash';
+import {
+ CustomObjectsApi,
+ HttpError,
+ KubeConfig,
+ KubernetesObject,
+ KubernetesListObject,
+ ListWatch,
+ V1ListMeta,
+ Watch,
+} from 'client-node-fixed-watcher';
+
+const EventEmitter = require('events');
+const log = require('./logger').Logger('watcher');
+
+// If listWatch errors out then we restart it after this many msecs.
+const RESTART_DELAY: number = 3000;
+// We wait this many msecs for an event confirming operation done previously.
+const EVENT_TIMEOUT: number = 5000;
+const GROUP: string = 'openebs.io';
+const VERSION: string = 'v1alpha1';
+
+// Errors generated by api requests are hopelessly useless. We need to add
+// a text from http body to them.
+function bodyError(prefix: string, error: any): any {
+ if (error instanceof HttpError) {
+ error.message = prefix + ': ' + error.body.message;
+ } else {
+ error.message = prefix + ': ' + error.message;
+ }
+ return error;
+}
+
+// Commonly used metadata attributes.
+export class CustomResourceMeta extends V1ListMeta {
+ name?: string;
+ namespace?: string;
+ generation?: number;
+ finalizers?: string[];
+}
+
+// Properties of custom resources (all optional so that we can do easy
+// conversion from "object" type)
+export class CustomResource implements KubernetesObject {
+ apiVersion?: string;
+ kind?: string;
+ metadata?: CustomResourceMeta;
+ spec?: object;
+ status?: any;
+}
+
+class TimeoutError extends Error {
+ constructor() {
+ super();
+ }
+}
+
+// Utility class for wrapping asynchronous operations that once done, need to be
+// confirmed by something from outside (i.e. watcher event). If confirmation does
+// not arrive on time, then end the operation regardless and let user know.
+class ConfirmOp {
+ private id: string;
+ private timer: NodeJS.Timeout | null;
+ private timeout: number;
+ private since: number;
+ private confirmed: boolean;
+ private done: boolean;
+ private resolve?: () => void;
+ private reject?: (err: any) => void;
+
+ constructor(id: string, timeout: number) {
+ this.id = id;
+ this.timeout = timeout;
+ this.since = 0;
+ this.timer = null;
+ this.confirmed = false;
+ this.done = false;
+ }
+
+ run(action: () => Promise): Promise {
+ this.since = (new Date()).getTime();
+ if (this.timeout <= 0) {
+ this.confirmed = true;
+ }
+ return new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ action()
+ .then(() => {
+ this.done = true;
+ if (!this.confirmed) {
+ this.timer = setTimeout(() => {
+ const delta = (new Date()).getTime() - this.since;
+ log.warn(`Timed out waiting for watcher event on "${this.id}" (${delta}ms)`);
+ this.timer = null;
+ reject(new TimeoutError());
+ }, this.timeout);
+ } else {
+ this._complete();
+ }
+ })
+ .catch((err) => {
+ this.done = true;
+ this._complete(err);
+ });
+ });
+ }
+
+ // Beware that confirm can come before the operation done callback!
+ confirm() {
+ this.confirmed = true;
+ if (this.timeout > 0) {
+ this._complete();
+ }
+ }
+
+ _complete(err?: any) {
+ if (!err && (!this.confirmed || !this.done)) return;
+
+ const delta = (new Date()).getTime() - this.since;
+ log.trace(`The operation on "${this.id}" took ${delta}ms`);
+ if (this.timer) {
+ clearTimeout(this.timer);
+ }
+ if (err) {
+ this.reject!(err);
+ } else {
+ this.resolve!();
+ }
+ }
+}
+
+// Resource cache keeps track of a k8s custom resource and exposes methods
+// for modifying the cache content.
+//
+// It is a classic operator loop design as seen in i.e. operator-sdk (golang)
+// to watch a k8s resource. We utilize k8s client library to take care of low
+// level details.
+//
+// It is a general implementation of watcher which can be used for any resource
+// operator. The operator should subscribe to "new", "mod" and "del" events that
+// are triggered when a resource is added, modified or deleted.
+export class CustomResourceCache extends EventEmitter {
+ name: string;
+ plural: string;
+ namespace: string;
+ waiting: Record;
+ k8sApi: CustomObjectsApi;
+ listWatch: ListWatch;
+ creator: new (obj: CustomResource) => T;
+ eventHandlers: Record void>;
+ connected: boolean;
+ restartDelay: number;
+ idleTimeout: number;
+ eventTimeout: number;
+ timer: any;
+
+ // Create the cache for given namespace and resource name.
+ //
+ // @param namespace Namespace of custom resource.
+ // @param name Name of the resource.
+ // @param kubeConfig Kube config object.
+ // @param creator Constructor of the object from custom resource object.
+ // @param opts Cache/watcher options.
+ constructor(
+ namespace: string,
+ name: string,
+ kubeConfig: KubeConfig,
+ creator: new (obj: CustomResource) => T,
+ opts?: {
+ restartDelay?: number,
+ eventTimeout?: number,
+ idleTimeout?: number
+ }
+ ) {
+ super();
+ this.k8sApi = kubeConfig.makeApiClient(CustomObjectsApi);
+ this.name = name;
+ this.plural = name + 's';
+ this.namespace = namespace;
+ this.creator = creator;
+ this.waiting = {};
+ this.connected = false;
+ this.restartDelay = opts?.restartDelay || RESTART_DELAY;
+ this.eventTimeout = opts?.eventTimeout || EVENT_TIMEOUT;
+ this.idleTimeout = opts?.idleTimeout || 0;
+ this.eventHandlers = {
+ add: this._onEvent.bind(this, 'new'),
+ update: this._onEvent.bind(this, 'mod'),
+ delete: this._onEvent.bind(this, 'del'),
+ };
+
+ const watch = new Watch(kubeConfig);
+ this.listWatch = new ListWatch(
+ `/apis/${GROUP}/${VERSION}/namespaces/${this.namespace}/${this.plural}`,
+ watch,
+ async () => {
+ var resp = await this.k8sApi.listNamespacedCustomObject(
+ GROUP,
+ VERSION,
+ this.namespace,
+ this.plural);
+ return {
+ response: resp.response,
+ body: resp.body as KubernetesListObject,
+ };
+ },
+ false
+ );
+ }
+
+ // Clear idle/restart timer.
+ _clearTimer() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ this.timer = undefined;
+ }
+ }
+ // Install a timer that restarts watcher if idle for more than x seconds.
+ // On Azure AKS we have observed watcher connections that don't get any
+ // events after some time when idle.
+ _setIdleTimeout() {
+ if (this.idleTimeout > 0) {
+ this._clearTimer();
+ this.timer = setTimeout(() => {
+ this.stop();
+ this.start();
+ }, this.idleTimeout);
+ }
+ }
+
+ // Called upon a watcher event. It unblocks create or update operation if any
+ // is waiting for the event and propagates the event further.
+ _onEvent(event: string, cr: CustomResource) {
+ let name = cr.metadata?.name;
+ if (name === undefined) {
+ log.error(`Ignoring event ${event} with object without a name`);
+ return;
+ }
+ log.trace(`Received watcher event ${event} for ${this.name} "${name}"`);
+ this._setIdleTimeout();
+ let confirmOp = this.waiting[name];
+ if (confirmOp) {
+ confirmOp.confirm();
+ }
+ this._doWithObject(cr, (obj) => this.emit(event, obj));
+ }
+
+ // Convert custom resource object to desired object swallowing exceptions
+ // and call callback with the new object.
+ _doWithObject(obj: CustomResource | undefined, cb: (obj: T) => void): void {
+ if (obj === undefined) return;
+
+ try {
+ var newObj = new this.creator(obj);
+ } catch (e) {
+ log.error(`Ignoring invalid ${this.name} custom resource: ${e}`);
+ return;
+ }
+ cb(newObj);
+ }
+
+ // This method does not return until the cache is successfully populated.
+ // That means that the promise eventually always fulfills (resolves).
+ start(): Promise {
+ this.listWatch.on('error', this._onError.bind(this));
+ for (let evName in this.eventHandlers) {
+ this.listWatch.on(evName, this.eventHandlers[evName]);
+ }
+ return this.listWatch.start()
+ .then(() => {
+ this.connected = true;
+ log.debug(`${this.name} watcher was started`);
+ log.trace(`Initial content of the "${this.name}" cache: ` +
+ this.listWatch.list().map((i: CustomResource) => i.metadata?.name));
+ this._setIdleTimeout();
+ })
+ .catch((err) => {
+ log.error(`Failed to start ${this.name} watcher: ${err}`)
+ this.stop();
+ log.info(`Restart ${this.name} watcher after ${this.restartDelay}ms...`);
+ return new Promise((resolve, reject) => {
+ this.timer = setTimeout(() => {
+ this.start().then(resolve, reject);
+ }, this.restartDelay);
+ });
+ });
+ }
+
+ // Called when the connection breaks.
+ _onError(err: any) {
+ log.error(`Watcher error: ${err}`);
+ this.stop();
+ log.info(`Restarting ${this.name} watcher after ${this.restartDelay}ms...`);
+ this.timer = setTimeout(() => this.start(), this.restartDelay);
+ }
+
+ // Deregister all internal event handlers on the watcher.
+ stop() {
+ this._clearTimer();
+ this.connected = false;
+ log.debug(`Deregistering "${this.name}" cache event handlers`);
+ this.listWatch.off('error', this._onError);
+ for (let evName in this.eventHandlers) {
+ this.listWatch.off(evName, this.eventHandlers[evName]);
+ }
+ this.listWatch.stop();
+ }
+
+ isConnected(): boolean {
+ // should we propagate event to consumers about the reset?
+ return this.connected;
+ }
+
+ // Get all objects from the cache.
+ list(): T[] {
+ let list: T[] = [];
+ this.listWatch.list().forEach((item) => {
+ this._doWithObject(item, (obj) => list.push(obj));
+ });
+ return list;
+ }
+
+ // Get object with given name (ID).
+ get(name: string): T | undefined {
+ var result;
+ this._doWithObject(this.listWatch.get(name), (obj) => result = obj);
+ return result;
+ }
+
+ // Execute the action and do not return until we receive an event from watcher.
+ // Otherwise the object in the cache might be stale when we do the next
+ // modification to it. Set timeout for the case when we never receive the
+ // event and restart the watcher to get fresh content in that case.
+ async _waitForEvent(name: string, action: () => Promise) {
+ this.waiting[name] = new ConfirmOp(name, this.eventTimeout);
+ try {
+ await this.waiting[name].run(action);
+ } catch (err) {
+ delete this.waiting[name];
+ if (err instanceof TimeoutError) {
+ // restart the cache
+ this.stop();
+ await this.start();
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ // Create the resource and wait for it to be created.
+ async create(obj: CustomResource) {
+ let name: string = obj.metadata?.name || '';
+ if (!name) {
+ throw Error("Object does not have a name");
+ }
+ log.trace(`Creating new "${this.name}" resource: ${JSON.stringify(obj)}`);
+ await this._waitForEvent(
+ name,
+ async () => {
+ try {
+ await this.k8sApi.createNamespacedCustomObject(
+ GROUP,
+ VERSION,
+ this.namespace,
+ this.plural,
+ obj
+ );
+ } catch (err) {
+ throw bodyError(`Delete of ${this.name} "${name}" failed`, err);
+ }
+ }
+ );
+ }
+
+ // Update the resource. The merge callback takes the original version from
+ // the cache, modifies it and returns the new version of object. The reason
+ // for this is that sometimes we get stale errors and we must repeat
+ // the operation with an updated version of the original object.
+ async update(name: string, merge: (orig: T) => CustomResource | undefined) {
+ await this._update(name, () => {
+ let orig = this.get(name);
+ if (orig === undefined) {
+ log.warn(`Tried to update ${this.name} "${name}" that does not exist`);
+ return;
+ }
+ return merge(orig);
+ });
+ }
+
+ // Same as above but works with custom resource type rather than user
+ // defined object.
+ async _updateCustomResource(name: string, merge: (orig: CustomResource) => CustomResource | undefined) {
+ await this._update(name, () => {
+ let orig = this.listWatch.get(name);
+ if (orig === undefined) {
+ log.warn(`Tried to update ${this.name} "${name}" that does not exist`);
+ return;
+ }
+ return merge(orig);
+ });
+ }
+
+ // Update the resource and wait for mod event. If update fails due to an error
+ // we restart the watcher and retry the operation. If event does not come,
+ // we restart the watcher.
+ async _update(
+ name: string,
+ getAndMerge: () => CustomResource | undefined,
+ ) {
+ for (let retries = 1; retries >= 0; retries -= 1) {
+ let obj = getAndMerge();
+ if (obj === undefined) {
+ // likely means that the props are the same - nothing to do
+ return;
+ }
+ log.trace(`Updating ${this.name} "${name}": ${JSON.stringify(obj)}`);
+ try {
+ await this._waitForEvent(
+ name,
+ async () => {
+ await this.k8sApi.replaceNamespacedCustomObject(
+ GROUP,
+ VERSION,
+ this.namespace,
+ this.plural,
+ name,
+ obj!
+ );
+ }
+ );
+ break;
+ } catch (err) {
+ err = bodyError(`Update of ${this.name} "${name}" failed`, err);
+ if (retries == 0) {
+ throw err;
+ }
+ log.warn(`${err} (retrying ...)`);
+ this.stop();
+ await this.start();
+ }
+ }
+ }
+
+ // Update status of the resource. Unlike in case create/update we don't have
+ // to wait for confirming event because generation number is not incremented
+ // upon status change.
+ async updateStatus(name: string, merge: (orig: T) => CustomResource | undefined) {
+ for (let retries = 1; retries >= 0; retries -= 1) {
+ let orig = this.get(name);
+ if (orig === undefined) {
+ log.warn(`Tried to update status of ${this.name} "${name}" but it is gone`);
+ return;
+ }
+ let obj = merge(orig);
+ if (obj === undefined) {
+ // likely means that the props are the same - nothing to do
+ return;
+ }
+ log.trace(`Updating status of ${this.name} "${name}": ${JSON.stringify(obj.status)}`);
+ try {
+ await this._waitForEvent(
+ name,
+ async () => {
+ await this.k8sApi.replaceNamespacedCustomObjectStatus(
+ GROUP,
+ VERSION,
+ this.namespace,
+ this.plural,
+ name,
+ obj!
+ );
+ }
+ );
+ break;
+ } catch (err) {
+ err = bodyError(`Status update of ${this.name} "${name}" failed`, err);
+ if (retries == 0) {
+ throw err;
+ }
+ log.warn(`${err} (retrying ...)`);
+ this.stop();
+ await this.start();
+ }
+ }
+ }
+
+ // Delete the resource.
+ async delete(name: string) {
+ let orig = this.get(name);
+ if (orig === undefined) {
+ log.warn(`Tried to delete ${this.name} "${name}" that does not exist`);
+ return new Promise((resolve) => resolve());
+ }
+ log.trace(`Deleting ${this.name} "${name}"`);
+ await this._waitForEvent(
+ name,
+ async () => {
+ try {
+ this.k8sApi.deleteNamespacedCustomObject(
+ GROUP,
+ VERSION,
+ this.namespace,
+ this.plural,
+ name
+ );
+ } catch (err) {
+ throw bodyError(`Delete of ${this.name} "${name}" failed`, err);
+ }
+ }
+ );
+ }
+
+ // Add finalizer to given resource if not already there.
+ async addFinalizer(name: string, finalizer: string) {
+ await this._updateCustomResource(name, (orig) => {
+ let finalizers = orig.metadata?.finalizers;
+ let newFinalizers = finalizers || [];
+ if (newFinalizers.indexOf(finalizer) >= 0) {
+ // it's already there
+ return;
+ }
+ newFinalizers = [finalizer].concat(newFinalizers);
+ let obj = _.cloneDeep(orig);
+ if (obj.metadata === undefined) {
+ throw new Error(`Resource ${this.name} "${name}" without metadata`)
+ }
+ obj.metadata.finalizers = newFinalizers;
+ return obj;
+ });
+ }
+
+ // Remove finalizer from the resource in case it's there.
+ async removeFinalizer(name: string, finalizer: string) {
+ await this._updateCustomResource(name, (orig) => {
+ let finalizers = orig.metadata?.finalizers;
+ let newFinalizers = finalizers || [];
+ let idx = newFinalizers.indexOf(finalizer);
+ if (idx < 0) {
+ // it's not there
+ return;
+ }
+ newFinalizers.splice(idx, 1);
+ let obj = _.cloneDeep(orig);
+ if (obj.metadata === undefined) {
+ throw new Error(`Resource ${this.name} "${name}" without metadata`)
+ }
+ obj.metadata.finalizers = newFinalizers;
+ return obj;
+ });
+ }
+}
diff --git a/csi/proto/mayastornodeplugin.proto b/csi/proto/mayastornodeplugin.proto
new file mode 100644
index 000000000..0e1977496
--- /dev/null
+++ b/csi/proto/mayastornodeplugin.proto
@@ -0,0 +1,61 @@
+// The definition of mayastor node plugin gRPC interface.
+// The node plugin service runs on all nodes running
+// the Mayastor CSI node plugin, and is complementary
+// to the CSI node plugin service.
+
+// This interface is supposed to be independent on particular computing
+// environment (i.e. kubernetes).
+
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.grpc.examples.mayastornodeplugin";
+option java_outer_classname = "MayastorNodePluginProto";
+
+package mayastornodeplugin;
+
+// Service for freezing and unfreezing file systems.
+service MayastorNodePlugin {
+ // Freeze the file system identified by the volume ID
+ // no check is made if the file system had been previously frozen.
+ rpc FreezeFS (FreezeFSRequest) returns (FreezeFSReply) {}
+ // Unfreeze the file system identified by the volume ID,
+ // no check is made if the file system had been previously frozen.
+ rpc UnfreezeFS (UnfreezeFSRequest) returns (UnfreezeFSReply) {}
+ // Find the volume identified by the volume ID, and return the mount type:
+ // raw block or filesystem
+ rpc FindVolume (FindVolumeRequest) returns (FindVolumeReply) {}
+}
+
+enum VolumeType {
+ VOLUME_TYPE_FILESYSTEM = 0; // File system mount
+ VOLUME_TYPE_RAWBLOCK = 1; // Raw block device mount
+}
+// The request message containing ID of the volume to be frozen
+message FreezeFSRequest {
+ string volume_id = 1;
+}
+
+// The response message for the freeze request.
+message FreezeFSReply {
+}
+
+// The request message containing ID of the volume to be unfrozen
+message UnfreezeFSRequest {
+
+ string volume_id = 1;
+}
+
+// The response message for the unfreeze request.
+message UnfreezeFSReply {
+}
+
+// Message for request on a volume
+message FindVolumeRequest {
+ string volume_id = 1;
+}
+
+// Message for response to a request for a volume
+message FindVolumeReply {
+ VolumeType volume_type = 1;
+}
diff --git a/csi/src/block_vol.rs b/csi/src/block_vol.rs
index 2902d2f97..227708c90 100644
--- a/csi/src/block_vol.rs
+++ b/csi/src/block_vol.rs
@@ -1,7 +1,6 @@
//! Functions for CSI publish and unpublish block mode volumes.
-use serde_json::Value;
-use std::{path::Path, process::Command};
+use std::path::Path;
use tonic::{Code, Status};
@@ -13,7 +12,7 @@ macro_rules! failure {
use crate::{
csi::*,
dev::Device,
- error::DeviceError,
+ findmnt,
mount::{self},
};
@@ -61,10 +60,10 @@ pub async fn publish_block_volume(
//target exists and is a special file
// Idempotency, if we have done this already just return success.
- match findmnt_device(target_path) {
+ match findmnt::get_devicepath(target_path) {
Ok(findmnt_dev) => {
if let Some(fm_devpath) = findmnt_dev {
- if equals_findmnt_device(&fm_devpath, &device_path) {
+ if fm_devpath == device_path {
debug!(
"{}({}) is already mounted onto {}",
fm_devpath, device_path, target_path
@@ -153,122 +152,3 @@ pub fn unpublish_block_volume(
info!("Volume {} unpublished from {}", volume_id, target_path);
Ok(())
}
-
-/// Keys of interest we expect to find in the JSON output generated
-/// by findmnt.
-const TARGET_KEY: &str = "target";
-const SOURCE_KEY: &str = "source";
-
-/// This function recurses over the de-serialised JSON returned by findmnt
-/// and searches for a target (file or directory) and returns the associated
-/// device if found.
-/// The assumptions made on the structure are:
-/// 1. An object has keys named "target" and "source" for a mount point.
-/// 2. An object may contain nested arrays of objects.
-///
-/// The search is deliberately generic (and hence slower) in an attempt to
-/// be more robust to future changes in findmnt.
-fn find_findmnt_target_device(
- json_val: &serde_json::value::Value,
- mountpoint: &str,
-) -> Result