diff --git a/examples/operatorhub_catalog/Cargo.toml b/examples/operatorhub_catalog/Cargo.toml new file mode 100644 index 0000000..df76819 --- /dev/null +++ b/examples/operatorhub_catalog/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "example-operatorhub-catalogsource" +edition = "2024" +version.workspace = true +readme.workspace = true +license.workspace = true +publish = false + +[dependencies] +harmony = { path = "../../harmony" } +harmony_cli = { path = "../../harmony_cli" } +harmony_types = { path = "../../harmony_types" } +cidr = { workspace = true } +tokio = { workspace = true } +harmony_macros = { path = "../../harmony_macros" } +log = { workspace = true } +env_logger = { workspace = true } +url = { workspace = true } diff --git a/examples/operatorhub_catalog/src/main.rs b/examples/operatorhub_catalog/src/main.rs new file mode 100644 index 0000000..fae94b7 --- /dev/null +++ b/examples/operatorhub_catalog/src/main.rs @@ -0,0 +1,23 @@ +use std::str::FromStr; + +use harmony::{ + inventory::Inventory, + modules::{k8s::apps::OperatorHubCatalogSourceScore, tenant::TenantScore}, + topology::{tenant::TenantConfig, K8sAnywhereTopology}, +}; +use harmony_types::id::Id; + +#[tokio::main] +async fn main() { + + let operatorhub_catalog = OperatorHubCatalogSourceScore::default(); + + harmony_cli::run( + Inventory::autoload(), + K8sAnywhereTopology::from_env(), + vec![Box::new(operatorhub_catalog)], + None, + ) + .await + .unwrap(); +} diff --git a/harmony/src/modules/k8s/apps/crd/catalogsources_operators_coreos_com.rs b/harmony/src/modules/k8s/apps/crd/catalogsources_operators_coreos_com.rs new file mode 100644 index 0000000..2f45b88 --- /dev/null +++ b/harmony/src/modules/k8s/apps/crd/catalogsources_operators_coreos_com.rs @@ -0,0 +1,157 @@ +use std::collections::BTreeMap; + +use k8s_openapi::{ + api::core::v1::{Affinity, Toleration}, + apimachinery::pkg::apis::meta::v1::ObjectMeta, +}; +use kube::CustomResource; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug)] +#[kube( + group = "operators.coreos.com", + version = "v1alpha1", + kind = "CatalogSource", + plural = "catalogsources", + namespaced = true, + schema = "disabled" +)] +#[serde(rename_all = "camelCase")] +pub struct CatalogSourceSpec { + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub config_map: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub grpc_pod_config: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub image: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub priority: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub publisher: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub run_as_root: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub secrets: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub source_type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub update_strategy: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GrpcPodConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub affinity: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub extract_content: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_target: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub node_selector: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub priority_class_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub security_context_config: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub tolerations: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct ExtractContent { + pub cache_dir: String, + pub catalog_dir: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct Icon { + pub base64data: String, + pub mediatype: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateStrategy { + #[serde(skip_serializing_if = "Option::is_none")] + pub registry_poll: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct RegistryPoll { + #[serde(skip_serializing_if = "Option::is_none")] + pub interval: Option, +} + +impl Default for CatalogSource { + fn default() -> Self { + Self { + metadata: ObjectMeta::default(), + spec: CatalogSourceSpec { + address: None, + config_map: None, + description: None, + display_name: None, + grpc_pod_config: None, + icon: None, + image: None, + priority: None, + publisher: None, + run_as_root: None, + secrets: None, + source_type: None, + update_strategy: None, + }, + } + } +} + +impl Default for CatalogSourceSpec { + fn default() -> Self { + Self { + address: None, + config_map: None, + description: None, + display_name: None, + grpc_pod_config: None, + icon: None, + image: None, + priority: None, + publisher: None, + run_as_root: None, + secrets: None, + source_type: None, + update_strategy: None, + } + } +} diff --git a/harmony/src/modules/k8s/apps/crd/mod.rs b/harmony/src/modules/k8s/apps/crd/mod.rs new file mode 100644 index 0000000..01bcf06 --- /dev/null +++ b/harmony/src/modules/k8s/apps/crd/mod.rs @@ -0,0 +1,4 @@ + +mod catalogsources_operators_coreos_com; +pub use catalogsources_operators_coreos_com::*; + diff --git a/harmony/src/modules/k8s/apps/mod.rs b/harmony/src/modules/k8s/apps/mod.rs new file mode 100644 index 0000000..59f34b9 --- /dev/null +++ b/harmony/src/modules/k8s/apps/mod.rs @@ -0,0 +1,4 @@ +mod operatorhub; +pub use operatorhub::*; +pub mod crd; + diff --git a/harmony/src/modules/k8s/apps/operatorhub.rs b/harmony/src/modules/k8s/apps/operatorhub.rs new file mode 100644 index 0000000..344aa61 --- /dev/null +++ b/harmony/src/modules/k8s/apps/operatorhub.rs @@ -0,0 +1,107 @@ +// Write operatorhub catalog score +// for now this will only support on OKD with the default catalog and operatorhub setup and does not verify OLM state or anything else. Very opinionated and bare-bones to start + +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use serde::Serialize; + +use crate::interpret::Interpret; +use crate::modules::k8s::apps::crd::{ + CatalogSource, CatalogSourceSpec, RegistryPoll, UpdateStrategy, +}; +use crate::modules::k8s::resource::K8sResourceScore; +use crate::score::Score; +use crate::topology::{K8sclient, Topology}; + +/// Installs the CatalogSource in a cluster which already has the required services and CRDs installed. +/// +/// ```rust +/// use harmony::modules::k8s::apps::OperatorHubCatalogSourceScore; +/// +/// let score = OperatorHubCatalogSourceScore::default(); +/// ``` +/// +/// Required services: +/// - catalog-operator +/// - olm-operator +/// +/// They are installed by default with OKD/Openshift +/// +/// **Warning** : this initial implementation does not manage the dependencies. They must already +/// exist in the cluster. +#[derive(Debug, Clone, Serialize)] +pub struct OperatorHubCatalogSourceScore { + pub name: String, + pub namespace: String, + pub image: String, +} + +impl OperatorHubCatalogSourceScore { + pub fn new(name: &str, namespace: &str, image: &str) -> Self { + Self { + name: name.to_string(), + namespace: namespace.to_string(), + image: image.to_string(), + } + } +} + +impl Default for OperatorHubCatalogSourceScore { + /// This default implementation will create this k8s resource : + /// + /// ```yaml + /// apiVersion: operators.coreos.com/v1alpha1 + /// kind: CatalogSource + /// metadata: + /// name: operatorhubio-catalog + /// namespace: openshift-marketplace + /// spec: + /// sourceType: grpc + /// image: quay.io/operatorhubio/catalog:latest + /// displayName: Operatorhub Operators + /// publisher: OperatorHub.io + /// updateStrategy: + /// registryPoll: + /// interval: 60m + /// ``` + fn default() -> Self { + OperatorHubCatalogSourceScore { + name: "operatorhubio-catalog".to_string(), + namespace: "openshift-marketplace".to_string(), + image: "quay.io/operatorhubio/catalog:latest".to_string(), + } + } +} + +impl Score for OperatorHubCatalogSourceScore { + fn create_interpret(&self) -> Box> { + let metadata = ObjectMeta { + name: Some(self.name.clone()), + namespace: Some(self.namespace.clone()), + ..ObjectMeta::default() + }; + + let spec = CatalogSourceSpec { + source_type: Some("grpc".to_string()), + image: Some(self.image.clone()), + display_name: Some("Operatorhub Operators".to_string()), + publisher: Some("OperatorHub.io".to_string()), + update_strategy: Some(UpdateStrategy { + registry_poll: Some(RegistryPoll { + interval: Some("60m".to_string()), + }), + }), + ..CatalogSourceSpec::default() + }; + + let catalog_source = CatalogSource { + metadata, + spec: spec, + }; + + K8sResourceScore::single(catalog_source, Some(self.namespace.clone())).create_interpret() + } + + fn name(&self) -> String { + format!("OperatorHubCatalogSourceScore({})", self.name) + } +} diff --git a/harmony/src/modules/k8s/mod.rs b/harmony/src/modules/k8s/mod.rs index 90781f9..7002d14 100644 --- a/harmony/src/modules/k8s/mod.rs +++ b/harmony/src/modules/k8s/mod.rs @@ -2,3 +2,4 @@ pub mod deployment; pub mod ingress; pub mod namespace; pub mod resource; +pub mod apps;