Skip to content

Commit

Permalink
(operator) Check for valid license (when license type is Online). (me…
Browse files Browse the repository at this point in the history
…talbear-co#2089)

* type for license validation

* tracing in operator status

* add logs for initialization + operator fetch

* more date related helpers for license + certificate

* cargo.lock

* change Days to u32

* improve validity // warn user if close to expiring

* docs

* remove commented code

* improve docs for Certificate

* return None on Expired

* move docs to operator

* clearer doc on operator session creation

* we, the license

Co-authored-by: Michał Smolarek <[email protected]>

* LicenseValidity trait

* LicenseValidity -> LicenseValidityV2

* fix tests // fix date math

* improve docs

* cargo lock

* cargo lock

* debug -> trace // fix days_until_expiration // changelog

* debug -> trace // changelog

---------

Co-authored-by: Michał Smolarek <[email protected]>
  • Loading branch information
meowjesty and Razz4780 authored Dec 12, 2023
1 parent 165114b commit d1c9045
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 11 deletions.
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions changelog.d/+operator-346.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adds a new trait `LicenseValidity` implemented for `DateTime` to help us when checking a license's validity. Relevant for [#346](https://github.com/metalbear-co/operator/issues/346).
28 changes: 27 additions & 1 deletion mirrord/auth/src/certificate.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{ops::Deref, str::FromStr};

use chrono::{DateTime, Utc};
use serde::{de, ser, Deserialize, Serialize};
use x509_certificate::{X509Certificate, X509CertificateError};

Expand All @@ -23,7 +24,19 @@ where
X509Certificate::from_pem(certificate).map_err(de::Error::custom)
}

/// Serializable `X509Certificate`
/// Serializable [`X509Certificate`]
///
/// Implements [`Deref`] into [`X509Certificate`], so you can take a `&Certificate` and call
/// `.as_ref()` on it to accesses the inner members of [`X509Certificate`], for example:
///
/// ```text
/// pub fn tbs(&self) {
/// self.0
/// .certificate
/// .as_ref()
/// .tbs_certificate; // accessed through the `AsRef` of `X509Certificate`
/// }
/// ```
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Certificate(
#[serde(
Expand All @@ -33,6 +46,19 @@ pub struct Certificate(
X509Certificate,
);

impl Certificate {
/// Extracts the expiration date (`not_after`) out of the certificate as a nice
/// `DateTime<Utc>`.
pub fn expiration_date(&self) -> DateTime<Utc> {
let validity = &self.0.as_ref().tbs_certificate.validity;

match validity.not_after.clone() {
x509_certificate::asn1time::Time::UtcTime(time) => *time,
x509_certificate::asn1time::Time::GeneralTime(time) => From::from(time),
}
}
}

impl From<X509Certificate> for Certificate {
fn from(certificate: X509Certificate) -> Self {
Certificate(certificate)
Expand Down
85 changes: 84 additions & 1 deletion mirrord/auth/src/credentials.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{fmt::Debug, ops::Deref};

use chrono::{DateTime, Utc};
use chrono::{DateTime, NaiveDate, NaiveTime, Utc};
use serde::{Deserialize, Serialize};
pub use x509_certificate;
use x509_certificate::{
Expand Down Expand Up @@ -70,6 +70,89 @@ impl AsRef<Certificate> for Credentials {
}
}

/// Extends a date type ([`DateTime<Utc>`]) to help us when checking for a license's
/// certificate validity.
pub trait LicenseValidity {
/// How many days we consider a license is close to expiring.
///
/// You can access this constant as
/// `<DateTime<Utc> as LicenseValidity>::CLOSE_TO_EXPIRATION_DAYS`.
const CLOSE_TO_EXPIRATION_DAYS: u64 = 2;

/// This date's validity is good.
fn is_good(&self) -> bool;

/// How many days until expiration from this date counting from _now_, which means that an
/// expiration date of `today + 3` means we have 2 days left until expiry.
fn days_until_expiration(&self) -> Option<u64>;

/// Converts a [`NaiveDate`] into a [`NaiveDateTime`], so we can turn it into a
/// [`DateTime<UTC>`] to check a license's validity.
///
/// I(alex) think this might cause trouble with potential mismatched timezone offsets, but
/// this is used only for a warning to the user.
fn from_naive_date(naive_date: NaiveDate) -> DateTime<Utc> {
let now = Utc::now();
let offset = *now.offset();
DateTime::<Utc>::from_naive_utc_and_offset(
naive_date
.and_time(NaiveTime::from_hms_opt(0, 0, 0).expect("Manually building valid date!")),
offset,
)
}
}

impl LicenseValidity for DateTime<Utc> {
fn is_good(&self) -> bool {
let now = Utc::now();
now < *self
}

fn days_until_expiration(&self) -> Option<u64> {
if self.is_good() {
let until_expiration = (*self - Utc::now()).num_days();

// We only want to return `Some(>= 0)`, never any negative numbers.
(0..=2)
.contains(&until_expiration)
.then_some(until_expiration as u64)
} else {
None
}
}
}

#[cfg(test)]
mod tests {
use chrono::{DateTime, Days, Utc};

use crate::credentials::LicenseValidity;

#[test]
fn license_validity_valid() {
let today: DateTime<Utc> = Utc::now();
let expiration_date = today.checked_add_days(Days::new(7)).unwrap();

assert!(expiration_date.is_good());
}

#[test]
fn license_validity_expired() {
let today: DateTime<Utc> = Utc::now();
let expiration_date = today.checked_sub_days(Days::new(7)).unwrap();

assert!(!expiration_date.is_good());
}

#[test]
fn license_validity_close_to_expiring() {
let today: DateTime<Utc> = Utc::now();
let expiration_date = today.checked_add_days(Days::new(3)).unwrap();

assert_eq!(expiration_date.days_until_expiration(), Some(2));
}
}

/// Ext trait for validation of dates of `rfc5280::Validity`
pub trait DateValidityExt {
/// Check other is in between not_before and not_after
Expand Down
10 changes: 10 additions & 0 deletions mirrord/cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ impl OperatorApiErrorExt for OperatorApiError {
}

/// Creates an agent if needed then connects to it.
///
/// First it checks if we have an `operator` in the [`config`](LayerConfig), which we do if the
/// user has installed the mirrord-operator in their cluster, even without a valid license. And
/// then we create a session with the operator with [`create_operator_session`].
///
/// If there is no operator, or the license is not good enough for starting an operator session,
/// then we create the mirrord-agent and run mirrord by itself, without the operator.
///
/// Here is where we start interactions with the kubernetes API.
#[tracing::instrument(level = "trace", skip_all)]
pub(crate) async fn create_and_connect<P>(
config: &LayerConfig,
progress: &mut P,
Expand Down
1 change: 1 addition & 0 deletions mirrord/cli/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ where
}

impl MirrordExecution {
#[tracing::instrument(level = "trace", skip_all)]
pub(crate) async fn start<P>(
config: &LayerConfig,
// We only need the executable on macos, for SIP handling.
Expand Down
1 change: 1 addition & 0 deletions mirrord/cli/src/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async fn get_status_api(config: Option<String>) -> Result<Api<MirrordOperatorCrd
Ok(Api::all(kube_api))
}

#[tracing::instrument(level = "trace", ret)]
async fn operator_status(config: Option<String>) -> Result<()> {
let mut progress = ProgressTracker::from_env("Operator Status");

Expand Down
1 change: 1 addition & 0 deletions mirrord/kube/src/api/kubernetes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ impl AgentManagment for KubernetesAPI {
))
}

#[tracing::instrument(level = "trace", skip(self, progress))]
async fn create_agent<P>(
&self,
progress: &mut P,
Expand Down
34 changes: 32 additions & 2 deletions mirrord/operator/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use std::io;

use base64::{engine::general_purpose, Engine as _};
use chrono::{DateTime, Utc};
use futures::{SinkExt, StreamExt};
use http::request::Request;
use kube::{api::PostParams, Api, Client, Resource};
use mirrord_analytics::{AnalyticsHash, AnalyticsOperatorProperties, AnalyticsReporter};
use mirrord_auth::{certificate::Certificate, credential_store::CredentialStoreSync};
use mirrord_auth::{
certificate::Certificate, credential_store::CredentialStoreSync, credentials::LicenseValidity,
};
use mirrord_config::{
feature::network::incoming::ConcurrentSteal,
target::{Target, TargetConfig},
Expand All @@ -22,7 +25,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio_tungstenite::tungstenite::{Error as TungsteniteError, Message};
use tracing::{debug, error};
use tracing::{debug, error, warn};

use crate::crd::{
CopyTargetCrd, CopyTargetSpec, MirrordOperatorCrd, OperatorFeatures, TargetCrd,
Expand Down Expand Up @@ -171,6 +174,11 @@ impl OperatorApi {
}

/// Creates new [`OperatorSessionConnection`] based on the given [`LayerConfig`].
/// Keep in mind that some failures here won't stop mirrord from hooking into the process
/// and working, it'll just work without the operator.
///
/// For a fuller documentation, see the docs in `operator/service/src/main.rs::listen`.
#[tracing::instrument(level = "trace", skip_all)]
pub async fn create_session<P>(
config: &LayerConfig,
progress: &P,
Expand All @@ -183,6 +191,25 @@ impl OperatorApi {

let operator = operator_api.fetch_operator().await?;

// Warns the user if their license is close to expiring.
//
// I(alex) considered doing a check for validity also here for expired licenses,
// but maybe the time of the local user and of the operator are out of sync, so we
// could end up blocking a valid license (or even just warning on it could be
// confusing).
if let Some(expiring_soon) =
DateTime::from_naive_date(operator.spec.license.expire_at).days_until_expiration()
&& (expiring_soon <= <DateTime<Utc> as LicenseValidity>::CLOSE_TO_EXPIRATION_DAYS)
{
let expiring_message = format!(
"Operator license will expire soon, in {} days!",
expiring_soon,
);

progress.warning(&expiring_message);
warn!(expiring_message);
}

Self::check_config(config, &operator)?;

let client_certificate =
Expand Down Expand Up @@ -315,6 +342,7 @@ impl OperatorApi {
})
}

#[tracing::instrument(level = "trace", skip(self), ret)]
async fn fetch_operator(&self) -> Result<MirrordOperatorCrd> {
let api: Api<MirrordOperatorCrd> = Api::all(self.client.clone());
api.get(OPERATOR_STATUS_NAME)
Expand All @@ -325,6 +353,8 @@ impl OperatorApi {
})
}

/// See `operator/controller/src/target.rs::TargetProvider::get_resource`.
#[tracing::instrument(level = "trace", fields(self.target_config), skip(self))]
async fn fetch_target(&self) -> Result<TargetCrd> {
let target_name = TargetCrd::target_name_by_config(&self.target_config);
self.target_api
Expand Down

0 comments on commit d1c9045

Please sign in to comment.