Skip to content

feat(stackable-versioned): Add conversion tracking #1056

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Techassi
Copy link
Member

@Techassi Techassi commented Jun 13, 2025

Part of stackabletech/issues#642

Tracking changes across sub structs

This is a rough sketch of what enabling this will look like. Support for this is essential so effectively track all changes values. Fields will be targeted by JSONPath like selectors.

use kube::CustomResource;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use stackable_versioned::{ChangedValues, versioned};

// A from trait which allows turning one type into another while also being able to track changes
// across upgrades and downgrades. This allows nested sub structs to bubble up their tracked changes.
pub trait TrackingFrom<T, S>
where
    Self: Sized,
    S: TrackingStatus,
{
    fn tracking_from(value: T, status: S) -> Self;
}

// This Into trait mirrors the standard Into trait, just with the ability to track changes. This
// trait is auto-implemented by all U: TrackingFrom.
pub trait TrackingInto<T, S>
where
    Self: Sized,
    S: TrackingStatus,
{
    fn tracking_into(self, status: S) -> T;
}

impl<T, U, S> TrackingInto<U, S> for T
where
    S: TrackingStatus,
    U: TrackingFrom<T, S>,
{
    fn tracking_into(self, status: S) -> U {
        U::tracking_from(self, status)
    }
}

// Generically access tracked changes in any status which implements this. The generated status
// struct will automatically implement this.
pub trait TrackingStatus {
    fn changes(&mut self) -> &mut ChangedValues;
}

#[versioned(
    version(name = "v1alpha1"),
    version(name = "v1beta1"),
    options(skip(from))
)]
mod versioned {
    #[versioned(
        k8s(
        group = "test.stackable.tech",
        status = FooStatus,
        // options(experimental_conversion_tracking)
    )
    )]
    #[derive(Clone, Debug, CustomResource, Serialize, Deserialize, JsonSchema)]
    pub struct FooSpec {
        // #[versioned(k8s(nested))]
        bar: Bar,
    }

    // #[versioned(k8s(nested))]
    #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
    pub struct Bar {
        baz: usize,

        #[versioned(added(since = "v1beta1"))]
        quox: u16,
    }
}

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct FooStatus {
    changes: ChangedValues,
}

// Those two impls are generated by default because FooSpec describes the root struct.
impl From<v1alpha1::Foo> for v1beta1::Foo {
    fn from(value: v1alpha1::Foo) -> Self {
        // Call spec.tracking_into(status), because at least one field was marked with k8s(nested)
        todo!()
    }
}

impl From<v1beta1::Foo> for v1alpha1::Foo {
    fn from(value: v1beta1::Foo) -> Self {
        todo!()
    }
}

// If k8s(nested) is applied to a field, we know that we instead need to generate the TrackingFrom
// impl instead of the standard From impl.
impl<S> TrackingFrom<v1alpha1::FooSpec, S> for v1beta1::FooSpec
where
    S: TrackingStatus,
{
    fn tracking_from(value: v1alpha1::FooSpec, status: S) -> Self {
        Self {
            // The following generation is already in place, just needs to be adjusted to be able
            // to generate tracking_into for fields which need it. Currently in always emits .into()
            bar: value.bar.tracking_into(status),
        }
    }
}

impl<S> TrackingFrom<v1beta1::FooSpec, S> for v1alpha1::FooSpec
where
    S: TrackingStatus,
{
    fn tracking_from(value: v1beta1::FooSpec, status: S) -> Self {
        Self {
            bar: value.bar.tracking_into(status),
        }
    }
}

// The following two impls are generated if k8s(nested) is set on a sub struct
impl<S> TrackingFrom<v1alpha1::Bar, S> for v1beta1::Bar
where
    S: TrackingStatus,
{
    fn tracking_from(value: v1alpha1::Bar, status: S) -> Self {
        // This is a nested sub struct and thus knows which fields change in what way. As such,
        // it can insert into and read from the provided status (which has access to a mutable
        // changes tracking section).

        Self {
            baz: value.baz,
            quox: Default::default(),
        }
    }
}

impl<S> TrackingFrom<v1beta1::Bar, S> for v1alpha1::Bar
where
    S: TrackingStatus,
{
    fn tracking_from(value: v1beta1::Bar, status: S) -> Self {
        Self { baz: value.baz }
    }
}

// This is to be generated automatically when conversion_tracking is enabled
impl TrackingStatus for FooStatus {
    fn changes(&mut self) -> &mut ChangedValues {
        &mut self.changes
    }
}

@Techassi Techassi self-assigned this Jun 13, 2025
@Techassi Techassi moved this to Development: In Progress in Stackable Engineering Jun 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Development: In Progress
Development

Successfully merging this pull request may close these issues.

1 participant