Skip to content

Commit b513919

Browse files
committed
refactor: extract apply step (Applier) and move secret creation into it
1 parent a22eb38 commit b513919

4 files changed

Lines changed: 210 additions & 132 deletions

File tree

rust/operator-binary/src/airflow_controller.rs

Lines changed: 21 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use snafu::{ResultExt, Snafu};
99
use stackable_operator::{
1010
cli::OperatorEnvironmentOptions,
1111
cluster_resources::ClusterResourceApplyStrategy,
12-
commons::random_secret_creation,
1312
k8s_openapi::api::core::v1::EnvVar,
1413
kube::{
1514
core::{DeserializeGuard, error_boundary},
@@ -21,19 +20,15 @@ use stackable_operator::{
2120
compute_conditions, operations::ClusterOperationsConditionBuilder,
2221
statefulset::StatefulSetConditionBuilder,
2322
},
24-
v2::cluster_resources::cluster_resources_new,
2523
};
2624
use strum::{EnumDiscriminants, IntoStaticStr};
2725

2826
use crate::{
29-
controller::{ValidatedCluster, build, controller_name, operator_name, product_name},
30-
crd::{
31-
AirflowClusterStatus, OPERATOR_NAME,
32-
internal_secret::{
33-
FERNET_KEY_SECRET_KEY, INTERNAL_SECRET_SECRET_KEY, JWT_SECRET_SECRET_KEY,
34-
},
35-
v1alpha2,
27+
controller::{
28+
apply::{self, Applier, ensure_random_secrets},
29+
build,
3630
},
31+
crd::{AirflowClusterStatus, OPERATOR_NAME, v1alpha2},
3732
};
3833

3934
pub const AIRFLOW_CONTROLLER_NAME: &str = "airflowcluster";
@@ -51,29 +46,20 @@ pub struct Ctx {
5146
#[strum_discriminants(derive(IntoStaticStr))]
5247
#[snafu(visibility(pub(crate)))]
5348
pub enum Error {
54-
#[snafu(display("failed to apply Kubernetes resource"))]
55-
ApplyResource {
56-
source: stackable_operator::cluster_resources::Error,
57-
},
49+
#[snafu(display("failed to apply the Kubernetes resources"))]
50+
ApplyResources { source: apply::Error },
5851

5952
#[snafu(display("failed to build the Kubernetes resources"))]
6053
BuildResources { source: build::Error },
6154

62-
#[snafu(display("failed to delete orphaned resources"))]
63-
DeleteOrphanedResources {
64-
source: stackable_operator::cluster_resources::Error,
65-
},
55+
#[snafu(display("failed to ensure the shared random Secrets exist"))]
56+
EnsureSecrets { source: apply::Error },
6657

6758
#[snafu(display("failed to update status"))]
6859
ApplyStatus {
6960
source: stackable_operator::client::Error,
7061
},
7162

72-
#[snafu(display("failed to create internal secret"))]
73-
InternalSecret {
74-
source: random_secret_creation::Error,
75-
},
76-
7763
#[snafu(display("failed to dereference cluster resources"))]
7864
Dereference {
7965
source: crate::controller::dereference::Error,
@@ -126,75 +112,25 @@ pub async fn reconcile_airflow(
126112
)
127113
.context(ValidateSnafu)?;
128114

129-
ensure_random_secrets(client, &validated_cluster).await?;
115+
let resources = build::build(&validated_cluster).context(BuildResourcesSnafu)?;
130116

131-
let mut cluster_resources = cluster_resources_new(
132-
&product_name(),
133-
&operator_name(),
134-
&controller_name(),
135-
&validated_cluster.name,
136-
&validated_cluster.namespace,
137-
&validated_cluster.uid,
117+
ensure_random_secrets(client, &validated_cluster)
118+
.await
119+
.context(EnsureSecretsSnafu)?;
120+
let applied = Applier::new(
121+
client,
122+
&validated_cluster,
138123
ClusterResourceApplyStrategy::from(&airflow.spec.cluster_operation),
139124
&airflow.spec.object_overrides,
140-
);
141-
142-
let resources = build::build(&validated_cluster).context(BuildResourcesSnafu)?;
125+
)
126+
.apply(resources)
127+
.await
128+
.context(ApplyResourcesSnafu)?;
143129

144130
let mut ss_cond_builder = StatefulSetConditionBuilder::default();
145-
146-
// Apply order is: StatefulSets last (a changed mounted ConfigMap/Secret
147-
// must exist first, else Pods restart -- commons-operator#111). The ServiceAccount comes
148-
// first because the Pods reference it at creation time.
149-
for service_account in resources.service_accounts {
150-
cluster_resources
151-
.add(client, service_account)
152-
.await
153-
.context(ApplyResourceSnafu)?;
154-
}
155-
for role_binding in resources.role_bindings {
156-
cluster_resources
157-
.add(client, role_binding)
158-
.await
159-
.context(ApplyResourceSnafu)?;
160-
}
161-
for service in resources.services {
162-
cluster_resources
163-
.add(client, service)
164-
.await
165-
.context(ApplyResourceSnafu)?;
166-
}
167-
for listener in resources.listeners {
168-
cluster_resources
169-
.add(client, listener)
170-
.await
171-
.context(ApplyResourceSnafu)?;
172-
}
173-
for config_map in resources.config_maps {
174-
cluster_resources
175-
.add(client, config_map)
176-
.await
177-
.context(ApplyResourceSnafu)?;
178-
}
179-
for pdb in resources.pod_disruption_budgets {
180-
cluster_resources
181-
.add(client, pdb)
182-
.await
183-
.context(ApplyResourceSnafu)?;
131+
for statefulset in applied.stateful_sets {
132+
ss_cond_builder.add(statefulset);
184133
}
185-
for statefulset in resources.stateful_sets {
186-
ss_cond_builder.add(
187-
cluster_resources
188-
.add(client, statefulset)
189-
.await
190-
.context(ApplyResourceSnafu)?,
191-
);
192-
}
193-
194-
cluster_resources
195-
.delete_orphaned_resources(client)
196-
.await
197-
.context(DeleteOrphanedResourcesSnafu)?;
198134

199135
let status = AirflowClusterStatus {
200136
conditions: compute_conditions(
@@ -211,50 +147,6 @@ pub async fn reconcile_airflow(
211147
Ok(Action::await_change())
212148
}
213149

214-
/// Ensures the three shared random Secrets (internal / JWT / Fernet) exist, creating any that are
215-
/// missing. These are read-or-create client operations, so they cannot be part of the client-free
216-
/// `build()` step.
217-
async fn ensure_random_secrets(
218-
client: &stackable_operator::client::Client,
219-
cluster: &ValidatedCluster,
220-
) -> Result<(), Error> {
221-
random_secret_creation::create_random_secret_if_not_exists(
222-
cluster.internal_secret_name().as_ref(),
223-
INTERNAL_SECRET_SECRET_KEY,
224-
256,
225-
cluster,
226-
client,
227-
)
228-
.await
229-
.context(InternalSecretSnafu)?;
230-
231-
random_secret_creation::create_random_secret_if_not_exists(
232-
cluster.jwt_secret_name().as_ref(),
233-
JWT_SECRET_SECRET_KEY,
234-
256,
235-
cluster,
236-
client,
237-
)
238-
.await
239-
.context(InternalSecretSnafu)?;
240-
241-
// https://airflow.apache.org/docs/apache-airflow/stable/security/secrets/fernet.html#security-fernet
242-
// does not document how long the fernet key should be, but recommends using
243-
// python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
244-
// which returns 32 bytes.
245-
random_secret_creation::create_random_secret_if_not_exists(
246-
cluster.fernet_key_name().as_ref(),
247-
FERNET_KEY_SECRET_KEY,
248-
32,
249-
cluster,
250-
client,
251-
)
252-
.await
253-
.context(InternalSecretSnafu)?;
254-
255-
Ok(())
256-
}
257-
258150
pub fn error_policy(
259151
_obj: Arc<DeserializeGuard<v1alpha2::AirflowCluster>>,
260152
error: &Error,
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//! The apply step in the AirflowCluster controller.
2+
3+
use std::marker::PhantomData;
4+
5+
use snafu::{ResultExt, Snafu};
6+
use stackable_operator::{
7+
client::Client,
8+
cluster_resources::{ClusterResource, ClusterResourceApplyStrategy, ClusterResources},
9+
commons::random_secret_creation,
10+
deep_merger::ObjectOverrides,
11+
v2::cluster_resources::cluster_resources_new,
12+
};
13+
use strum::{EnumDiscriminants, IntoStaticStr};
14+
15+
use crate::{
16+
controller::{
17+
Applied, KubernetesResources, Prepared, ValidatedCluster, controller_name, operator_name,
18+
product_name,
19+
},
20+
crd::internal_secret::{
21+
FERNET_KEY_SECRET_KEY, INTERNAL_SECRET_SECRET_KEY, JWT_SECRET_SECRET_KEY,
22+
},
23+
};
24+
25+
#[derive(Snafu, Debug, EnumDiscriminants)]
26+
#[strum_discriminants(derive(IntoStaticStr))]
27+
pub enum Error {
28+
#[snafu(display("failed to apply Kubernetes resource"))]
29+
ApplyResource {
30+
source: stackable_operator::cluster_resources::Error,
31+
},
32+
33+
#[snafu(display("failed to delete orphaned resources"))]
34+
DeleteOrphanedResources {
35+
source: stackable_operator::cluster_resources::Error,
36+
},
37+
38+
#[snafu(display("failed to create internal secret"))]
39+
InternalSecret {
40+
source: random_secret_creation::Error,
41+
},
42+
}
43+
44+
type Result<T, E = Error> = std::result::Result<T, E>;
45+
46+
/// Applier for the Kubernetes resource specifications produced by this controller.
47+
///
48+
/// The implementation is not tied to this controller and could theoretically be moved to
49+
/// stackable_operator if [`KubernetesResources`] would contain all possible resource types.
50+
pub struct Applier<'a> {
51+
client: &'a Client,
52+
cluster_resources: ClusterResources<'a>,
53+
}
54+
55+
impl<'a> Applier<'a> {
56+
pub fn new(
57+
client: &'a Client,
58+
cluster: &ValidatedCluster,
59+
apply_strategy: ClusterResourceApplyStrategy,
60+
object_overrides: &'a ObjectOverrides,
61+
) -> Applier<'a> {
62+
let cluster_resources = cluster_resources_new(
63+
&product_name(),
64+
&operator_name(),
65+
&controller_name(),
66+
&cluster.name,
67+
&cluster.namespace,
68+
&cluster.uid,
69+
apply_strategy,
70+
object_overrides,
71+
);
72+
73+
Applier {
74+
client,
75+
cluster_resources,
76+
}
77+
}
78+
79+
/// Applies the given Kubernetes resources and marks them as applied.
80+
pub async fn apply(
81+
mut self,
82+
resources: KubernetesResources<Prepared>,
83+
) -> Result<KubernetesResources<Applied>> {
84+
// Apply order is: StatefulSets last (a changed mounted ConfigMap/Secret
85+
// must exist first, else Pods restart -- commons-operator#111). The ServiceAccount comes
86+
// first because the Pods reference it at creation time.
87+
let service_accounts = self.add_resources(resources.service_accounts).await?;
88+
let role_bindings = self.add_resources(resources.role_bindings).await?;
89+
let services = self.add_resources(resources.services).await?;
90+
let listeners = self.add_resources(resources.listeners).await?;
91+
let config_maps = self.add_resources(resources.config_maps).await?;
92+
let pod_disruption_budgets = self.add_resources(resources.pod_disruption_budgets).await?;
93+
let stateful_sets = self.add_resources(resources.stateful_sets).await?;
94+
95+
self.cluster_resources
96+
.delete_orphaned_resources(self.client)
97+
.await
98+
.context(DeleteOrphanedResourcesSnafu)?;
99+
100+
Ok(KubernetesResources {
101+
stateful_sets,
102+
services,
103+
listeners,
104+
config_maps,
105+
pod_disruption_budgets,
106+
service_accounts,
107+
role_bindings,
108+
status: PhantomData,
109+
})
110+
}
111+
112+
async fn add_resources<T: ClusterResource + Sync>(
113+
&mut self,
114+
resources: Vec<T>,
115+
) -> Result<Vec<T>> {
116+
let mut applied_resources = vec![];
117+
118+
for resource in resources {
119+
let applied_resource = self
120+
.cluster_resources
121+
.add(self.client, resource)
122+
.await
123+
.context(ApplyResourceSnafu)?;
124+
applied_resources.push(applied_resource);
125+
}
126+
127+
Ok(applied_resources)
128+
}
129+
}
130+
131+
/// Ensures the three shared random Secrets (internal / JWT / Fernet) exist, creating any that are
132+
/// missing. These are read-or-create client operations, so they cannot be part of the client-free
133+
/// `build()` step; they are also deliberately not tracked in [`ClusterResources`], so they survive
134+
/// orphan deletion and an existing Secret is never overwritten.
135+
pub async fn ensure_random_secrets(client: &Client, cluster: &ValidatedCluster) -> Result<()> {
136+
random_secret_creation::create_random_secret_if_not_exists(
137+
cluster.internal_secret_name().as_ref(),
138+
INTERNAL_SECRET_SECRET_KEY,
139+
256,
140+
cluster,
141+
client,
142+
)
143+
.await
144+
.context(InternalSecretSnafu)?;
145+
146+
random_secret_creation::create_random_secret_if_not_exists(
147+
cluster.jwt_secret_name().as_ref(),
148+
JWT_SECRET_SECRET_KEY,
149+
256,
150+
cluster,
151+
client,
152+
)
153+
.await
154+
.context(InternalSecretSnafu)?;
155+
156+
// https://airflow.apache.org/docs/apache-airflow/stable/security/secrets/fernet.html#security-fernet
157+
// does not document how long the fernet key should be, but recommends using
158+
// python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
159+
// which returns 32 bytes.
160+
random_secret_creation::create_random_secret_if_not_exists(
161+
cluster.fernet_key_name().as_ref(),
162+
FERNET_KEY_SECRET_KEY,
163+
32,
164+
cluster,
165+
client,
166+
)
167+
.await
168+
.context(InternalSecretSnafu)?;
169+
170+
Ok(())
171+
}

0 commit comments

Comments
 (0)