diff --git a/.github/workflows/helm-oci.yml b/.github/workflows/helm-oci.yml new file mode 100644 index 0000000..1a818b2 --- /dev/null +++ b/.github/workflows/helm-oci.yml @@ -0,0 +1,34 @@ +name: Docker + +on: + schedule: + - cron: "15 14 * * *" + push: + paths: + - "charts/**" + + branches: ["main", "staging"] + # Publish semver tags as releases. + tags: ["v*.*.*"] + pull_request: + branches: ["main"] + workflow_dispatch: + +jobs: + docker: + uses: appany/helm-oci-chart-releaser@v0.3.0 + with: + name: chisel-operator + repository: chisel-operator + tag: 0.1.0 + path: charts/chisel-operator + registry: ghcr.io + registry_username: ${{ github.actor }} + registry_password: ${{ secrets.GITHUB_TOKEN }} + update_dependencies: 'true' # Defaults to false + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write diff --git a/Cargo.lock b/Cargo.lock index e6e8352..94e2a23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chisel-operator" -version = "0.3.0" +version = "0.4.0" dependencies = [ "async-trait", "aws-config", diff --git a/Cargo.toml b/Cargo.toml index e417418..06da673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chisel-operator" -version = "0.3.0" +version = "0.4.0" edition = "2021" description = "Chisel tunnel operator for Kubernetes" authors = [ diff --git a/charts/chisel-operator/templates/crds/exit-node.yaml b/charts/chisel-operator/templates/crds/exit-node.yaml index 2229607..42e3e55 100644 --- a/charts/chisel-operator/templates/crds/exit-node.yaml +++ b/charts/chisel-operator/templates/crds/exit-node.yaml @@ -15,7 +15,7 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: [] - name: v1 + name: v1.1 schema: openAPIV3Schema: description: Auto-generated derived type for ExitNodeSpec via `CustomResource` @@ -68,20 +68,22 @@ spec: provider: type: string service_binding: - nullable: true - properties: - name: - type: string - namespace: - type: string - required: - - name - - namespace - type: object + items: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + type: array required: - ip - name - provider + - service_binding type: object required: - spec diff --git a/deploy/crd/exit-node.yaml b/deploy/crd/exit-node.yaml index 1adef8c..5ecbcd9 100644 --- a/deploy/crd/exit-node.yaml +++ b/deploy/crd/exit-node.yaml @@ -13,7 +13,7 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: [] - name: v1 + name: v1.1 schema: openAPIV3Schema: description: Auto-generated derived type for ExitNodeSpec via `CustomResource` @@ -66,20 +66,22 @@ spec: provider: type: string service_binding: - nullable: true - properties: - name: - type: string - namespace: - type: string - required: - - name - - namespace - type: object + items: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + type: array required: - ip - name - provider + - service_binding type: object required: - spec diff --git a/src/cloud/aws.rs b/src/cloud/aws.rs index b0dbad1..6340c5c 100644 --- a/src/cloud/aws.rs +++ b/src/cloud/aws.rs @@ -206,13 +206,21 @@ impl Provisioner for AWSProvisioner { } }; - let exit_node = ExitNodeStatus { - name: name.clone(), - ip: public_ip, - id: Some(instance.instance_id.unwrap()), - provider: provisioner.clone(), - service_binding: None, - }; + // let exit_node = ExitNodeStatus { + // name: name.clone(), + // ip: public_ip, + // id: Some(instance.instance_id.unwrap()), + // provider: provisioner.clone(), + // service_binding: vec![], + // }; + let exit_node = ExitNodeStatus::new( + provisioner.clone(), + name.clone(), + public_ip, + // needless conversion? + // todo: Clean this up, minor performance hit + instance.instance_id.map(|id| id.to_string()).as_deref(), + ); Ok(exit_node) } diff --git a/src/cloud/digitalocean.rs b/src/cloud/digitalocean.rs index b29da2c..d819c2e 100644 --- a/src/cloud/digitalocean.rs +++ b/src/cloud/digitalocean.rs @@ -131,13 +131,12 @@ impl Provisioner for DigitalOceanProvisioner { } }; - let exit_node = ExitNodeStatus { - name: name.clone(), - ip: droplet_ip.clone(), - id: Some(droplet.id.to_string()), - provider: provisioner.clone(), - service_binding: None, - }; + let exit_node = ExitNodeStatus::new( + provisioner.clone(), + name.clone(), + droplet_ip.clone(), + Some(&droplet_id), + ); Ok(exit_node) } diff --git a/src/cloud/linode.rs b/src/cloud/linode.rs index 822d023..fbb4123 100644 --- a/src/cloud/linode.rs +++ b/src/cloud/linode.rs @@ -111,13 +111,20 @@ impl Provisioner for LinodeProvisioner { } }; - let status = ExitNodeStatus { - ip: instance_ip, - name: instance.label, - provider: provisioner.to_string(), - id: Some(instance.id.to_string()), - service_binding: None, - }; + // let status = ExitNodeStatus { + // ip: instance_ip, + // name: instance.label, + // provider: provisioner.to_string(), + // id: Some(instance.id.to_string()), + // service_binding: vec![], + // }; + + let status = ExitNodeStatus::new( + instance_ip, + instance.label, + provisioner.to_string(), + Some(&instance.id.to_string()), + ); Ok(status) } diff --git a/src/daemon.rs b/src/daemon.rs index 617715e..e0ff4b8 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -24,13 +24,16 @@ use color_eyre::Result; use futures::{FutureExt, StreamExt}; -use k8s_openapi::api::{ - apps::v1::Deployment, - core::v1::{LoadBalancerIngress, LoadBalancerStatus, Service, ServiceStatus}, +use k8s_openapi::{ + api::{ + apps::v1::Deployment, + core::v1::{LoadBalancerIngress, LoadBalancerStatus, Service, ServiceStatus}, + }, + Metadata, }; use kube::{ api::{Api, ListParams, Patch, PatchParams, ResourceExt}, - core::ObjectMeta, + core::{object::HasStatus, ObjectMeta}, error::ErrorResponse, runtime::{ controller::Action, @@ -174,10 +177,11 @@ async fn select_exit_node_local( }) .unwrap_or(false); + // Should return true if there's any service bindings in the status let has_service_binding = node .status .as_ref() - .map(|status| status.service_binding.is_some()) + .map(|status| !status.service_binding.is_empty()) .unwrap_or(false); // Is the ExitNode not cloud provisioned or is the status set? And (in both cases) does it not have a service binding? @@ -185,7 +189,8 @@ async fn select_exit_node_local( }) .collect::>() .first() - .ok_or(ReconcileError::NoAvailableExitNodes).cloned() + .ok_or(ReconcileError::NoAvailableExitNodes) + .cloned() } } @@ -304,12 +309,17 @@ async fn reconcile_svcs(obj: Arc, ctx: Arc) -> Result, ctx: Arc) -> Result, ctx: Arc) -> Result = match event { Event::Apply(svc) => { - let node_data = serde_json::json!({ - "status": { - "service_binding": ServiceBinding { - namespace: svc.namespace().unwrap(), - name: svc.name_any() - } - } - }); + + // service_binding is a vec of ServiceBinding, so use the current one and add the new one if not already present + + + // let node_data = serde_json::json!({ + // "status": { + // "service_binding": ServiceBinding { + // namespace: svc.namespace().unwrap(), + // name: svc.name_any() + // } + // } + // }); + + let mut node_data = node.clone(); + + + // Template for service binding + let svc_binding = ServiceBinding { + namespace: svc.namespace().unwrap(), + name: svc.name_any(), + }; + + + // if status is None, set it to the new service binding + if node_data.status().is_none() { + node_data.status = Some(ExitNodeStatus { + provider: UNMANAGED_PROVISIONER.to_string(), + name: node_data.name_any(), + ip: exit_node_ip.clone(), + id: None, + service_binding: vec![svc_binding.clone()], + }); + } + + // if status is Some, add the new service binding to the existing ones + if let Some(status) = node_data.status.as_mut() { + status.service_binding.push(svc_binding); + } + + // Now, let's finally patch the status! + + + let _nodes = namespaced_nodes .patch_status( // We can unwrap safely since Service is guaranteed to have a name @@ -515,7 +560,7 @@ async fn reconcile_nodes(obj: Arc, ctx: Arc) -> Result, ctx: Arc) -> Result = - Api::namespaced(ctx.client.clone(), &binding.namespace); - - let patch = serde_json::json!({ - "status": { - "load_balancer": None:: - } - }); - - let _svc = services - .patch_status( - // We can unwrap safely since Service is guaranteed to have a name - &binding.name, - &serverside.clone(), - &Patch::Merge(patch), - ) - .await?; - - info!("Cleared service binding for {}", node.name_any()); + // if let Some(binding) = &status.service_binding { + // info!("Clearing service binding for {}", node.name_any()); + + // // get service API + // let services: Api = + // Api::namespaced(ctx.client.clone(), &binding.namespace); + + // let patch = serde_json::json!({ + // "status": { + // "load_balancer": None:: + // } + // }); + + // let _svc = services + // .patch_status( + // // We can unwrap safely since Service is guaranteed to have a name + // &binding.name, + // &serverside.clone(), + // &Patch::Merge(patch), + // ) + // .await?; + + // info!("Cleared service binding for {}", node.name_any()); + // } + + if !&status.service_binding.is_empty() { + for binding in &status.service_binding { + info!("Clearing service binding for {}", node.name_any()); + + // get service API + let services: Api = + Api::namespaced(ctx.client.clone(), &binding.namespace); + + let patch = serde_json::json!({ + "status": { + "load_balancer": None:: + } + }); + + let _svc = services + .patch_status( + // We can unwrap safely since Service is guaranteed to have a name + &binding.name, + &serverside.clone(), + &Patch::Merge(patch), + ) + .await?; + + info!("Cleared service binding for {}", node.name_any()); + } } } diff --git a/src/ops.rs b/src/ops.rs index 96281a0..6b918a2 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -29,7 +29,7 @@ pub fn parse_provisioner_label_value<'a>( #[derive(Serialize, Deserialize, Debug, CustomResource, Clone, JsonSchema)] #[kube( group = "chisel-operator.io", - version = "v1", + version = "v1.1", kind = "ExitNode", singular = "exitnode", struct = "ExitNode", @@ -135,8 +135,30 @@ pub struct ExitNodeStatus { // pub password: String, pub ip: String, pub id: Option, - pub service_binding: Option, + pub service_binding: Vec, +} + +impl ExitNodeStatus { + pub fn find_svc_binding(&self, namespace: &str, name: &str) -> Option { + self.service_binding + .iter() + .find(|svc| svc.namespace == namespace && svc.name == name) + .cloned() + } + + // It is indeed being used in cloud/* + #[allow(unused_variables)] + pub fn new(provider: String, name: String, ip: String, id: Option<&str>) -> Self { + Self { + provider, + name, + ip, + id: None, + service_binding: vec![], + } + } } + #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] pub struct ServiceBinding {