Repository: comtrya/comtrya (language: rust)

Issue:
FR: Properly deal with `apt-key add` deprecation
## The Background
`apt-key add` is being deprecated.

Quoting the [The Debian WIKI](https://wiki.debian.org/DebianRepository/UseThirdParty):
> ... The key MUST NOT be placed in /etc/apt/trusted.gpg.d or loaded by apt-key add.

Here is a rather good article that explains how to add keys properly:
> https://www.linuxuprising.com/2021/01/apt-key-is-deprecated-how-to-add.html

It is also reflected in the more-up-to-date vendor's documentation, like google-cloud-cli ([link](https://cloud.google.com/sdk/docs/install#debianubuntu)):
> If your distribution (Debian 11+ or Ubuntu 21.10+) doesn't support apt-key, run the following command:
> ```
> curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo tee /usr/share/keyrings/cl
> ```

The above is just to state, that this has '_become a thing_'.

## The request:
Using `apt-key` should be replaced with the process of adding a key to `/usr/share/keyrings` and then referring to it in the '/etc/apt/sources.d/repo-name.list` file.
Maybe even adding an option to mention a GPG fingerprint (or better yet, a URL to a FINGERPRINT) to be downloaded.

## Some examples
An example of grabbing the gpg file and storing it (note: will require sudo)
```
wget -O- <https://example.com/key/repo-key.gpg> | gpg --dearmor | sudo tee /usr/share/keyrings/<myrepository>-archive-keyring.gpg
```
followed by
```
sudo apt-add-repository "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/<myrepository>-archive-keyring.gpg] https://repository.example.com/debian/ $(lsb_release -cs) stable main "
```




Discussion / hints:
I had no idea. Thank you, @Lockszmith-GH - we’ll fix this 
A [good explanation](https://stackoverflow.com/a/71384057/799379) of the 'why' behind the deprecation - in case someone is interested:


Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 2f1b84f4..2d10394d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,6 +39,7 @@ tracing-subscriber = "0.3.11"
 walkdir = "2"
 which = "4.2.5"
 whoami = "1.2.1"
+sha256 = "1.0.3"
 
 [target.'cfg(unix)'.dependencies]
 users = "0.11.0"
diff --git a/src/actions/package/install.rs b/src/actions/package/install.rs
index 369c5074..f1c58a4a 100644
--- a/src/actions/package/install.rs
+++ b/src/actions/package/install.rs
@@ -37,12 +37,6 @@ impl Action for PackageInstall {
             atoms.append(&mut provider.bootstrap());
         }
 
-        if let Some(ref _repo) = variant.repository {
-            if !provider.has_repository(&variant) {
-                atoms.append(&mut provider.add_repository(&variant));
-            }
-        }
-
         atoms.append(&mut provider.install(&variant));
 
         span.exit();
diff --git a/src/actions/package/mod.rs b/src/actions/package/mod.rs
index 1fec3cf7..a2a07dfc 100644
--- a/src/actions/package/mod.rs
+++ b/src/actions/package/mod.rs
@@ -1,5 +1,6 @@
 pub mod install;
 pub mod providers;
+mod repository;
 
 use providers::PackageProviders;
 use serde::{Deserialize, Serialize};
@@ -39,12 +40,6 @@ pub struct PackageVariant {
     #[serde(default)]
     provider: PackageProviders,
 
-    #[serde(default)]
-    repository: Option<String>,
-
-    #[serde(default)]
-    key: Option<String>,
-
     #[serde(default)]
     extra_args: Vec<String>,
 }
@@ -71,8 +66,6 @@ impl From<&Package> for PackageVariant {
                 name: package.name.clone(),
                 list: package.list.clone(),
                 provider: package.provider.clone(),
-                repository: package.repository.clone(),
-                key: package.key.clone(),
                 extra_args: package.extra_args.clone(),
             };
         };
@@ -85,8 +78,6 @@ impl From<&Package> for PackageVariant {
             name: package.name.clone(),
             list: package.list.clone(),
             provider: variant.provider.clone(),
-            repository: variant.repository.clone(),
-            key: package.key.clone(),
             extra_args: variant.extra_args.clone(),
         };
 
@@ -98,14 +89,6 @@ impl From<&Package> for PackageVariant {
             package.list = variant.list.clone();
         }
 
-        if variant.repository.is_some() {
-            package.repository = variant.repository.clone();
-        };
-
-        if variant.key.is_some() {
-            package.key = variant.key.clone();
-        }
-
         // I've been torn about this, but here's my logic.
         // Variants, when being used, shouldn't use the provider
         // of the main definition; as we're not the core OS.
diff --git a/src/actions/package/providers/aptitude.rs b/src/actions/package/providers/aptitude.rs
index d88e3f49..9c70b99a 100644
--- a/src/actions/package/providers/aptitude.rs
+++ b/src/actions/package/providers/aptitude.rs
@@ -1,8 +1,9 @@
 use super::PackageProvider;
-use crate::actions::package::PackageVariant;
+use crate::actions::package::{repository::PackageRepository, PackageVariant};
 use crate::atoms::command::Exec;
 use crate::steps::Step;
 use serde::{Deserialize, Serialize};
+use sha256::digest;
 use tracing::warn;
 use which::which;
 
@@ -53,25 +54,27 @@ impl PackageProvider for Aptitude {
         }]
     }
 
-    fn has_repository(&self, _package: &PackageVariant) -> bool {
+    fn has_repository(&self, _: &PackageRepository) -> bool {
         false
     }
 
-    fn add_repository(&self, package: &PackageVariant) -> Vec<Step> {
-        if package.repository.is_none() {
-            return vec![];
-        }
-
+    fn add_repository(&self, repository: &PackageRepository) -> Vec<Step> {
         let mut steps: Vec<Step> = vec![];
 
-        if package.key.is_some() {
+        let mut signed_by = String::from("");
+
+        if repository.key.is_some() {
+            let key = repository.clone().key.unwrap();
+
+            let key_name = key.name.unwrap_or(digest(&key.url));
+            let key_path = format!("/usr/sharekeyrings/{}.asc", key_name);
+
+            signed_by = format!("signed-by={}", key_path);
+
             steps.push(Step {
                 atom: Box::new(Exec {
-                    command: String::from("bash"),
-                    arguments: vec![
-                        String::from("-c"),
-                        format!("curl {} | apt-key add -", package.key.clone().unwrap()),
-                    ],
+                    command: String::from("curl"),
+                    arguments: vec![String::from("-o"), key_path, key.url],
                     environment: self.env(),
                     privileged: true,
                     ..Default::default()
@@ -81,11 +84,19 @@ impl PackageProvider for Aptitude {
             });
         }
 
+        //sudo apt-add-repository "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/<myrepository>-archive-keyring.gpg] https://repository.example.com/debian/ $(lsb_release -cs) stable main "
         steps.extend(vec![
             Step {
                 atom: Box::new(Exec {
                     command: String::from("apt-add-repository"),
-                    arguments: vec![String::from("-y"), package.repository.clone().unwrap()],
+                    arguments: vec![
+                        String::from("-y"),
+                        format!(
+                            "deb [arch=$(dpkg --print-architecture) {}] {}",
+                            signed_by,
+                            repository.name.clone()
+                        ),
+                    ],
                     environment: self.env(),
                     privileged: true,
                     ..Default::default()
@@ -134,50 +145,52 @@ impl PackageProvider for Aptitude {
 
 #[cfg(test)]
 mod test {
+    use crate::actions::package::repository::RepositoryKey;
+
     use super::*;
 
     // These tests are really weak at the moment, but that's because I'm not
     // sure how to add derive(Debug,Default) to struct Step
     // TODO: Learn how to fix this
 
-    #[test]
-    fn test_add_repository_simple() {
-        let package = PackageVariant {
-            name: Some(String::from("test")),
-            ..Default::default()
-        };
-
-        let aptitude = Aptitude {};
-        let steps = aptitude.add_repository(&package);
-
-        assert_eq!(steps.len(), 0);
-    }
-
     #[test]
     fn test_add_repository_without_key() {
-        let package = PackageVariant {
-            name: Some(String::from("test")),
-            repository: Some(String::from("repository")),
-            ..Default::default()
-        };
-
         let aptitude = Aptitude {};
-        let steps = aptitude.add_repository(&package);
+        let steps = aptitude.add_repository(&PackageRepository {
+            name: String::from("test"),
+            ..Default::default()
+        });
 
         assert_eq!(steps.len(), 2);
     }
 
     #[test]
     fn test_add_repository_with_key() {
-        let package = PackageVariant {
-            name: Some(String::from("test")),
-            repository: Some(String::from("repository")),
-            key: Some(String::from("key")),
+        let aptitude = Aptitude {};
+        let steps = aptitude.add_repository(&PackageRepository {
+            name: String::from("test"),
+            key: Some(RepositoryKey {
+                url: String::from("abc"),
+                ..Default::default()
+            }),
             ..Default::default()
-        };
+        });
 
+        assert_eq!(steps.len(), 3);
+    }
+
+    #[test]
+    fn test_add_repository_with_key_and_fingerprint() {
         let aptitude = Aptitude {};
-        let steps = aptitude.add_repository(&package);
+        let steps = aptitude.add_repository(&PackageRepository {
+            name: String::from("test"),
+            key: Some(RepositoryKey {
+                url: String::from("abc"),
+                fingerprint: Some(String::from("abc")),
+                ..Default::default()
+            }),
+            ..Default::default()
+        });
 
         assert_eq!(steps.len(), 3);
     }
diff --git a/src/actions/package/providers/bsdpkg.rs b/src/actions/package/providers/bsdpkg.rs
index 8cf1ac7d..78530c4c 100644
--- a/src/actions/package/providers/bsdpkg.rs
+++ b/src/actions/package/providers/bsdpkg.rs
@@ -1,4 +1,5 @@
 use super::PackageProvider;
+use crate::actions::package::repository::PackageRepository;
 use crate::steps::finalizers::FlowControl::StopIf;
 use crate::steps::finalizers::OutputContains;
 use crate::steps::Step;
@@ -46,11 +47,11 @@ impl PackageProvider for BsdPkg {
         }]
     }
 
-    fn has_repository(&self, _package: &PackageVariant) -> bool {
+    fn has_repository(&self, _: &PackageRepository) -> bool {
         false
     }
 
-    fn add_repository(&self, _package: &PackageVariant) -> Vec<Step> {
+    fn add_repository(&self, _: &PackageRepository) -> Vec<Step> {
         vec![]
     }
 
diff --git a/src/actions/package/providers/dnf.rs b/src/actions/package/providers/dnf.rs
index 87dd1c98..9ee29896 100644
--- a/src/actions/package/providers/dnf.rs
+++ b/src/actions/package/providers/dnf.rs
@@ -1,6 +1,6 @@
 use super::PackageProvider;
 
-use crate::actions::package::PackageVariant;
+use crate::actions::package::{repository::PackageRepository, PackageVariant};
 use crate::atoms::command::Exec;
 use crate::steps::Step;
 use serde::{Deserialize, Serialize};
@@ -42,22 +42,20 @@ impl PackageProvider for Dnf {
         }]
     }
 
-    fn has_repository(&self, _package: &PackageVariant) -> bool {
+    fn has_repository(&self, _: &PackageRepository) -> bool {
         false
     }
 
-    fn add_repository(&self, package: &PackageVariant) -> Vec<Step> {
-        if package.repository.is_none() {
-            return vec![];
-        }
-
+    fn add_repository(&self, repository: &PackageRepository) -> Vec<Step> {
         let mut steps: Vec<Step> = vec![];
 
-        if package.key.is_some() {
+        if repository.key.is_some() {
+            let key = repository.clone().key.unwrap();
+
             steps.extend(vec![Step {
                 atom: Box::new(Exec {
                     command: String::from("rpm"),
-                    arguments: vec![String::from("--import"), package.key.clone().unwrap()],
+                    arguments: vec![String::from("--import"), key.url],
                     privileged: true,
                     ..Default::default()
                 }),
@@ -74,7 +72,7 @@ impl PackageProvider for Dnf {
                         String::from("config-manager"),
                         String::from("--assumeyes"),
                         String::from("--add-repo"),
-                        package.repository.clone().unwrap(),
+                        repository.name.clone(),
                     ],
                     privileged: true,
                     ..Default::default()
@@ -129,46 +127,34 @@ impl PackageProvider for Dnf {
 
 #[cfg(test)]
 mod test {
-    use super::*;
-
-    #[test]
-    fn test_add_repository_simple() {
-        let package = PackageVariant {
-            name: Some(String::from("test")),
-            ..Default::default()
-        };
+    use crate::actions::package::{providers::PackageProviders, repository::RepositoryKey};
 
-        let dnf = Dnf {};
-        let steps = dnf.add_repository(&package);
-
-        assert_eq!(steps.len(), 0);
-    }
+    use super::*;
 
     #[test]
     fn test_add_repository_without_key() {
-        let package = PackageVariant {
-            name: Some(String::from("test")),
-            repository: Some(String::from("repository")),
-            ..Default::default()
-        };
-
         let dnf = Dnf {};
-        let steps = dnf.add_repository(&package);
+        let steps = dnf.add_repository(&PackageRepository {
+            name: String::from("test"),
+            provider: PackageProviders::Dnf,
+            ..Default::default()
+        });
 
         assert_eq!(steps.len(), 2);
     }
 
     #[test]
     fn test_repository_with_key() {
-        let package = PackageVariant {
-            name: Some(String::from("test")),
-            repository: Some(String::from("repository")),
-            key: Some(String::from("key")),
-            ..Default::default()
-        };
-
         let dnf = Dnf {};
-        let steps = dnf.add_repository(&package);
+        let steps = dnf.add_repository(&PackageRepository {
+            name: String::from("test"),
+            key: Some(RepositoryKey {
+                url: String::from("abc"),
+                ..Default::default()
+            }),
+            provider: PackageProviders::Dnf,
+            ..Default::default()
+        });
 
         assert_eq!(steps.len(), 3);
     }
diff --git a/src/actions/package/providers/homebrew.rs b/src/actions/package/providers/homebrew.rs
index d1bc5ed4..9dcbdd4f 100644
--- a/src/actions/package/providers/homebrew.rs
+++ b/src/actions/package/providers/homebrew.rs
@@ -1,4 +1,5 @@
 use super::PackageProvider;
+use crate::actions::package::repository::PackageRepository;
 use crate::steps::Step;
 use crate::{actions::package::PackageVariant, atoms::command::Exec};
 use serde::{Deserialize, Serialize};
@@ -29,20 +30,18 @@ impl PackageProvider for Homebrew {
         }), initializers: vec![], finalizers: vec![] },]
     }
 
-    fn has_repository(&self, _: &PackageVariant) -> bool {
+    fn has_repository(&self, _: &PackageRepository) -> bool {
         // Brew doesn't make it easy to check if the repository is already added
         // except by running `brew tap` and grepping.
         // Fortunately, adding an exist tap is pretty fast.
         false
     }
 
-    fn add_repository(&self, package: &PackageVariant) -> Vec<Step> {
-        let repository = package.repository.clone().unwrap();
-
+    fn add_repository(&self, repository: &PackageRepository) -> Vec<Step> {
         vec![Step {
             atom: Box::new(Exec {
                 command: String::from("brew"),
-                arguments: vec![String::from("tap"), repository],
+                arguments: vec![String::from("tap"), repository.name.clone()],
                 ..Default::default()
             }),
             initializers: vec![],
@@ -80,10 +79,6 @@ impl PackageProvider for Homebrew {
                     true
                 }
             })
-            .map(|p| match &package.repository {
-                Some(repository) => format!("{}/{}", repository, p),
-                None => p,
-            })
             .collect()
     }
 
diff --git a/src/actions/package/providers/mod.rs b/src/actions/package/providers/mod.rs
index 863b7743..72effcb7 100644
--- a/src/actions/package/providers/mod.rs
+++ b/src/actions/package/providers/mod.rs
@@ -13,7 +13,7 @@ mod yay;
 use self::yay::Yay;
 mod winget;
 use self::winget::Winget;
-use super::PackageVariant;
+use super::{repository::PackageRepository, PackageVariant};
 use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -92,8 +92,8 @@ pub trait PackageProvider {
     fn name(&self) -> &str;
     fn available(&self) -> bool;
     fn bootstrap(&self) -> Vec<Step>;
-    fn has_repository(&self, package: &PackageVariant) -> bool;
-    fn add_repository(&self, package: &PackageVariant) -> Vec<Step>;
+    fn has_repository(&self, package: &PackageRepository) -> bool;
+    fn add_repository(&self, package: &PackageRepository) -> Vec<Step>;
     fn query(&self, package: &PackageVariant) -> Vec<String>;
     fn install(&self, package: &PackageVariant) -> Vec<Step>;
 }
diff --git a/src/actions/package/providers/pkgin.rs b/src/actions/package/providers/pkgin.rs
index 077a4ffe..33d2b7f6 100644
--- a/src/actions/package/providers/pkgin.rs
+++ b/src/actions/package/providers/pkgin.rs
@@ -1,4 +1,5 @@
 use super::PackageProvider;
+use crate::actions::package::repository::PackageRepository;
 use crate::steps::finalizers::FlowControl::StopIf;
 use crate::steps::finalizers::OutputContains;
 use crate::steps::Step;
@@ -32,11 +33,11 @@ impl PackageProvider for Pkgin {
         vec![]
     }
 
-    fn has_repository(&self, _package: &PackageVariant) -> bool {
+    fn has_repository(&self, _: &PackageRepository) -> bool {
         false
     }
 
-    fn add_repository(&self, _package: &PackageVariant) -> Vec<Step> {
+    fn add_repository(&self, _: &PackageRepository) -> Vec<Step> {
         vec![]
     }
 
diff --git a/src/actions/package/providers/winget.rs b/src/actions/package/providers/winget.rs
index 662be97e..cc0a8879 100644
--- a/src/actions/package/providers/winget.rs
+++ b/src/actions/package/providers/winget.rs
@@ -1,4 +1,5 @@
 use super::PackageProvider;
+use crate::actions::package::repository::PackageRepository;
 use crate::steps::Step;
 use crate::{actions::package::PackageVariant, atoms::command::Exec};
 use serde::{Deserialize, Serialize};
@@ -27,11 +28,11 @@ impl PackageProvider for Winget {
         vec![]
     }
 
-    fn has_repository(&self, _package: &PackageVariant) -> bool {
+    fn has_repository(&self, _: &PackageRepository) -> bool {
         true
     }
 
-    fn add_repository(&self, _package: &PackageVariant) -> Vec<Step> {
+    fn add_repository(&self, _: &PackageRepository) -> Vec<Step> {
         vec![]
     }
 
diff --git a/src/actions/package/providers/yay.rs b/src/actions/package/providers/yay.rs
index 92e1f736..2d658ccc 100644
--- a/src/actions/package/providers/yay.rs
+++ b/src/actions/package/providers/yay.rs
@@ -1,4 +1,5 @@
 use super::PackageProvider;
+use crate::actions::package::repository::PackageRepository;
 use crate::actions::package::PackageVariant;
 use crate::atoms::command::Exec;
 use crate::steps::Step;
@@ -70,11 +71,11 @@ impl PackageProvider for Yay {
         ]
     }
 
-    fn has_repository(&self, _package: &PackageVariant) -> bool {
+    fn has_repository(&self, _: &PackageRepository) -> bool {
         false
     }
 
-    fn add_repository(&self, _package: &PackageVariant) -> Vec<Step> {
+    fn add_repository(&self, _: &PackageRepository) -> Vec<Step> {
         vec![]
     }
 
diff --git a/src/actions/package/repository.rs b/src/actions/package/repository.rs
new file mode 100644
index 00000000..f6dd9659
--- /dev/null
+++ b/src/actions/package/repository.rs
@@ -0,0 +1,102 @@
+use crate::actions::Action;
+use crate::contexts::Contexts;
+use crate::manifests::Manifest;
+use crate::steps::Step;
+use serde::{Deserialize, Serialize};
+use std::ops::Deref;
+use tracing::{error, span};
+
+use super::providers::PackageProviders;
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+pub struct PackageRepository {
+    #[serde(alias = "url")]
+    pub name: String,
+
+    pub key: Option<RepositoryKey>,
+
+    #[serde(default)]
+    pub provider: PackageProviders,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+pub struct RepositoryKey {
+    pub url: String,
+    pub name: Option<String>,
+    pub key: Option<String>,
+    pub fingerprint: Option<String>,
+}
+
+impl Action for PackageRepository {
+    fn plan(&self, _manifest: &Manifest, _context: &Contexts) -> Vec<Step> {
+        let box_provider = self.provider.clone().get_provider();
+        let provider = box_provider.deref();
+
+        let span = span!(
+            tracing::Level::INFO,
+            "package.repository",
+            provider = provider.name()
+        )
+        .entered();
+
+        let mut atoms: Vec<Step> = vec![];
+
+        // If the provider isn't available, see if we can bootstrap it
+        if !provider.available() {
+            if provider.bootstrap().is_empty() {
+                error!(
+                    "Package Provider, {}, isn't available. Skipping action",
+                    provider.name()
+                );
+                return vec![];
+            }
+
+            atoms.append(&mut provider.bootstrap());
+        }
+
+        if !provider.has_repository(&self) {
+            atoms.append(&mut provider.add_repository(&self));
+        }
+
+        span.exit();
+
+        atoms
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use crate::actions::Actions;
+
+//     #[test]
+//     fn it_can_be_deserialized() {
+//         let yaml = r#"
+// - action: package.install
+//   name: curl
+
+// - action: package.install
+//   list:
+//     - bash
+// "#;
+
+//         let mut actions: Vec<Actions> = serde_yaml::from_str(yaml).unwrap();
+
+//         match actions.pop() {
+//             Some(Actions::PackageInstall(action)) => {
+//                 assert_eq!(vec!["bash"], action.action.list);
+//             }
+//             _ => {
+//                 panic!("PackageInstall didn't deserialize to the correct type");
+//             }
+//         };
+
+//         match actions.pop() {
+//             Some(Actions::PackageInstall(action)) => {
+//                 assert_eq!("curl", action.action.name.unwrap());
+//             }
+//             _ => {
+//                 panic!("PackageInstall didn't deserialize to the correct type");
+//             }
+//         };
+//     }
+// }


Test patch: