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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//! Information about a running container
//!
//! This is going to use shiplift since they seem to have done most of the work already.
//! This is independent of docker because there are multiple ways of manipulating these (docker,
//! docker-compose, etc.) so we're going to make a specific container to hold the metadata.
//! THINK: Should this assume it is clean (freshly spun up) or can it be dirty?

const APP_NAME: &str = "Docker Container";
const MODULE_VERSION: &'static str = env!("CARGO_PKG_VERSION");

use anyhow::{Context, Result};
use serde_derive::{Deserialize, Serialize};
// use schemars::JsonSchema;
// use shiplift::Docker;

use super::*;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Status {
  Down,
  /// Have not looked up the status
  Unknown,
  /// The container is currently running
  Up,
  /// The container is being reported as done
  Exited,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerContainer {
  pub status: Status,
  pub instance: AppInstance,

  /// The container who owns this instance, and how we send manipulation commands (eg Docker, DockerCompose)
  #[serde(skip)]
  pub parent: Option<Rc<dyn ContainerTrait>>,

  /// The shell to use inside this container when running additional executables
  #[serde(skip)]
  pub shell: Option<Rc<dyn ContainerTrait>>,
}

impl DockerContainer {
  /// If we don't have a parent, set it to a local instance of docker
  /// TODO: Actually make this happen. Currently already using Docker-Compose
  pub fn set_parent(&self, parent: Rc<dyn ContainerTrait>) -> Result<DockerContainer> {
    match &self.parent {
      Some(x) => log::info!(
        "Replacing parent {} on container {} with {}",
        x.get_name(),
        self.get_name(),
        parent.get_name()
      ),
      None => (),
    }
    Ok(DockerContainer {
      parent: Some(parent),
      ..self.clone()
    })
  }

  /// Find and verify the shell on the container
  pub fn set_shell(&self, preferred: Option<AppQuery>) -> Result<DockerContainer> {
    let query = preferred.unwrap_or(AppQuery::new("bash".to_string()));

    match &self.shell {
      Some(x) => log::info!(
        "Replacing shell {} on container {} with {}",
        x.get_name(),
        self.get_name(),
        query.name
      ),
      None => (),
    }

    // HACK: Still trying to get my head around this special case
    let shell = Rc::new(Bash::build(AppInstance::new("bash".to_string()), None)?);

    Ok(DockerContainer {
      shell: Some(shell),
      ..self.clone()
    })
  }

  fn get_module_version() -> Result<semver::Version> {
    Ok({
      semver::Version::parse(MODULE_VERSION).context(format!(
        "{} has an invalid version number '{}' Cargo.toml",
        APP_NAME, MODULE_VERSION
      ))
    }?)
  }

  fn get_name(&self) -> String {
    match &self.instance.version {
      Some(ver) => format!("{} ({})", APP_NAME, ver),
      None => format!("{} (Unknown Version)", APP_NAME),
    }
  }
}

impl AppTrait for DockerContainer {
  fn get_name(&self) -> String {
    self.get_name()
  }

  fn build(
    instance: AppInstance,
    parent: Option<Rc<dyn ContainerTrait>>,
  ) -> Result<DockerContainer> {
    let base = DockerContainer {
      status: Status::Down,
      instance: AppInstance {
        module_version: Some(DockerContainer::get_module_version()?),
        ..instance.clone()
      },
      parent,
      shell: None,
    };
    base.set_shell(None)
  }

  // /// Knows how to get the version number of the installed app (not the module version)
  // fn set_version(&self, _instance: AppInstance) -> Result<AppInstance> {
  //   unimplemented!()
  // }
  // /// Figures out how to call the cli using the given container
  // fn set_cli(
  //   &self,
  //   _instance: AppInstance,
  //   _container: Rc<dyn ContainerTrait>,
  // ) -> Result<AppInstance> {
  //   unimplemented!()
  // }
}

impl ContainerTrait for DockerContainer {
  /// This will find a list of apps with configurations that the container knows about
  fn find(&self, query: AppQuery) -> Result<Vec<AppInstance>> {
    // Is there a shell
    let shell = match &self.shell {
      None => Err(FoundryError::NotConfigured).context(format!(
        "Cannot run find: No shell set in '{}'",
        self.get_name()
      ))?,
      Some(x) => x,
    };

    // Is there a parent container
    let parent = match &self.parent {
      None => Err(FoundryError::NotConfigured).context(format!(
        "Cannot run find: No parent set in '{}'",
        self.get_name()
      ))?,
      Some(x) => x,
    };

    // HACK: This should be generic container work, but not quite sure how to make it so

    let regex = regex::Regex::new(r"^(\w+) \(([\w\.\s]+)\)$")?;
    let lc = shell.get_name().to_lowercase();
    let name = regex.captures(&lc).map_or(
      Err(FoundryError::UnexpectedValue).context(format!(
        "'{}' does not appear to be a valid shell name",
        shell.get_name()
      )),
      |cap| {
        cap.get(1).map_or(
          Err(FoundryError::UnexpectedValue).context(format!(
            "'{}' does not appear to be a valid shell name",
            shell.get_name()
          )),
          |val| Ok(val.as_str()),
        )
      },
    )?;

    let cmd = match name {
      "bash" => Ok(Message::Command(Cmd {
        run_as: None,
        command: "bash".to_string(),
        args: ["-c", &format!("command -v {}", query.name)]
          .iter()
          .map(|x| x.to_string())
          .collect(),
      })),
      _ => Err(FoundryError::UnexpectedValue).context(format!(
        "Docker containers are not currently set to use '{}' shells",
        name
      )),
    }?;

    let location = parent.forward(self.instance.clone(), cmd)?;
    Ok(vec![AppInstance::new(query.name.clone())
      .set_command_path(Some(Rc::new(self.clone())), location)?])
  }

  /// List the known items in the app cache
  fn cached_apps(&self) -> Result<Vec<AppInstance>> {
    unimplemented!("No App Cache for Bash Yet")
  }

  fn forward(&self, _to: AppInstance, message: Message) -> Result<String> {
    // Just send it along to
    match &self.parent {
      None => Err(FoundryError::NotConfigured).context(format!(
        "Parent isn't set up for forwarding on container {}",
        self.instance.name
      )),
      Some(x) => x.forward(self.instance.clone(), message),
    }
  }

  /// Get the name/version of the container, usually for use in logging/errors.
  fn get_name(&self) -> String {
    self.get_name()
  }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FindApp(AppQuery);

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Action {
  Find(FindApp),
  Inspect(InspectOptions),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ActionResult {
  FindResult(Vec<AppInstance>),
  InspectResult,
}

impl Action {
  fn _run(&self, _container: DockerContainer) -> Result<ActionResult> {
    // We shouldn't be able to run anything without a valid configuration
    // let conf = match &compose.config {
    //   Some(conf) => conf,
    //   None => Err(FoundryError::ConfigurationError).context(format!(
    //     "Docker Compose tried to run action {:#?} without a valid config",
    //     self
    //   ))?,
    // };

    // if status is not Run, start and flag

    // if container.parent is empty, assume there is a local docker to use

    // if container.shell is empty, attempt to look up bash in the container

    match self {
      Action::Inspect(_) => unimplemented!("Next up, Inspect"),
      Action::Find(_query) => unimplemented!(),
    }
  }
}

impl ActionTrait for FindApp {
  type RESPONSE = ActionResult;

  fn run(&self, _target: AppInstance) -> Result<Self::RESPONSE> {
    unimplemented!()
  }

  fn to_message(&self, _target: Option<AppInstance>) -> Result<Vec<Message>> {
    unimplemented!("ActionTrait not implemented for shell")
  }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InspectOptions {}

impl ActionTrait for InspectOptions {
  type RESPONSE = ActionResult;

  fn run(&self, _target: AppInstance) -> Result<Self::RESPONSE> {
    unimplemented!()
  }

  fn to_message(&self, _target: Option<AppInstance>) -> Result<Vec<Message>> {
    unimplemented!("ActionTrait not implemented for shell")
  }
}