1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
//! A wrapper around external software to generate TPF Actions and Events
//!
//! An application (app) is an interface which defines a set of functions to convert TPF actions into native
//! commands, monitor state, and sends out messages describing any changes in state.
//!
//! At its core, an application is something that executes actions and emits events. It has no context of
//! anything outside the scope of its functionality. It can be accessed by one or more containers, depending
//! on the context.
//!
//! For more detailed info about the architecture, read
//! https://lurkingfrog.github.io/the_process_foundry_book/core/application.html

use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::rc::Rc;

use super::FoundryError;
use super::{AppQuery, ContainerTrait};

/// A **data structure** containing information used to generate both generate Actions and Events
///
///
pub trait AppTrait {
  /// Construct the metadata for the module controlling the app instance
  /// This is important to make sure the module aligns with the actual installed instance
  fn build(instance: AppInstance, parent: Option<Rc<dyn ContainerTrait>>) -> Result<Self>
  where
    Self: Sized;

  /// Print a standardized "name (version)" for logging purposes
  fn get_name(&self) -> String;

  /// Knows how to get the version number of the installed app (not the module version)
  fn set_version(&self, instance: AppInstance) -> Result<AppInstance> {
    log::warn!(
      "Set version has not been implemented for {}",
      self.get_name()
    );
    Ok(instance)
  }

  // /// Figures out how to call the cli using the given container
  // /// THINK: is it better to have an option or make "Local" a special case?
  // fn set_cli(
  //   &self,
  //   instance: AppInstance,
  //   container: Rc<dyn ContainerTrait>,
  // ) -> Result<AppInstance>;

  // list_actions
}

/// General information about the external application handled by this particular module
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Serialize, Deserialize)]
pub struct AppDescription {
  /// The name the external application is known by
  name: String,

  /// The versions of the external app that are handled by this App
  handles_versions: Option<semver::VersionReq>,

  // ****  Some query helper information  *** //
  /// Names that this app might also be known as
  ///
  /// # Examples: postgres may also be known as `pg` or `postgresql`
  aliases: Option<Vec<String>>,

  /// Some standard paths to look for information
  search_paths: Option<Vec<String>>,
  // Network polling? ports, process name, remote_ip?
}

impl std::fmt::Display for AppDescription {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "{:#?}", self)
  }
}

impl AppDescription {
  /// Use the description to create a query to find running instances of this app
  pub fn to_app_query(&self) -> AppQuery {
    AppQuery {
      name: self.name.clone(),
      works_with: self.handles_versions.clone(),
      aliases: self.aliases.clone(),
      search_paths: self.search_paths.clone(),
      ..Default::default()
    }
  }
}

/// Information about the specific running copy of the external program
///
/// This is a synthesis of both introspected information (version) and external (ip address/port)
/// THINK: Should this identify the environment?
/// THINK: Should the app instance know itself/functions?
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct AppInstance {
  /// This is for use by Containers enumerating instances of their content
  // THINK: Use hash value of instance? https://doc.rust-lang.org/std/hash/index.html#examples
  pub instance_id: Option<String>,

  /// The standard name for this app (e.g. Postgres, DockerCompose)
  pub name: String,

  /// The version of the installed app
  /// NOTE: Some items (like sh), don't easily have a version available, so we make it optional
  pub version: Option<semver::Version>,

  /// The version of the Foundry code running
  /// NOTE: This will be much more important once TPF becomes distributed microservices
  pub module_version: Option<semver::Version>,

  pub config_file: Option<String>,

  // pub codebase: Option<Box<dyn AppTrait>>,
  pub cli: Option<CliAccess>,
  pub api: Option<ApiAccess>,
}

impl AppInstance {
  pub fn new(name: String) -> AppInstance {
    AppInstance {
      name,
      ..Default::default()
    }
  }

  pub fn full_name(&self) -> String {
    match &self.version {
      Some(ver) => format!("{} ({})", self.name, ver),
      None => format!("{} ({})", self.name, "Unknown Version"),
    }
  }

  pub fn set_command_path(
    &self,
    container: Option<Rc<dyn ContainerTrait>>,
    path: String,
  ) -> Result<AppInstance> {
    let cli = self.cli.clone().map_or(
      CliAccess {
        path: path.clone(),
        container: container.clone(),
      },
      |cli| CliAccess {
        path: path.clone(),
        ..cli.clone()
      },
    );

    Ok(AppInstance {
      cli: Some(cli),
      ..self.clone()
    })
  }

  pub fn get_command_path(&self) -> Result<String> {
    match &self.cli {
      None => Err(FoundryError::NotConfigured).context(format!("Cli is not set for {}", self.name)),
      Some(cli) => Ok(cli.path.clone()),
    }
  }
}

impl std::fmt::Debug for AppInstance {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(
      f,
      "AppInstance {{
        name: {},
        version: {:#?},
        config_file: {:#?},
        cli: {}
        network: {}
      }}",
      self.name,
      self.version,
      self.config_file,
      self
        .cli
        .clone()
        .map_or("None".to_string(), |cli| cli.path.clone()),
      self.api.clone().map_or("None", |_net| {
        "Networking not implemented yet for AppInstance"
      }),
    )
  }
}

impl std::fmt::Display for AppInstance {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "AppInstance {} ({:#?})", self.name, self.version)
  }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CliAccess {
  /// The container where this App is found
  #[serde(skip)]
  pub container: Option<Rc<dyn ContainerTrait>>,

  /// The location of the executable
  pub path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiAccess {
  pub uri: String,
}