Skip to content

Commit 7339bee

Browse files
committed
refactor: Split into Deployment and DaemonSet
The splits the deployment of the secret-operator as a whole into two parts: - The controller is deployed via a Deployment which ensures only a single instance of the secret-operator in controller mode is running in a Kubernetes cluster. This can potentially lead to perfomance issues and as such should be monitored going forward. - The CSI server is deployed via a DaemonSet (unchanged) as this server is needed on every node to provision requested secret volumes. This refactor is introduced in preparation for #634, in which only a single instance of the CRD conversion webhook must exist as otherwise TLS certificate verification will fail with multiple available certificates.
1 parent aa59ecd commit 7339bee

5 files changed

Lines changed: 223 additions & 83 deletions

File tree

Tiltfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ if os.path.exists('result'):
2626
# oci.stackable.tech/sandbox/opa-operator:7y19m3d8clwxlv34v5q2x4p7v536s00g instead of
2727
# oci.stackable.tech/sandbox/opa-operator:0.0.0-dev (which does not exist)
2828
k8s_kind('Deployment', image_json_path='{.spec.template.metadata.annotations.internal\\.stackable\\.tech/image}')
29+
k8s_kind('DaemonSet', image_json_path='{.spec.template.metadata.annotations.internal\\.stackable\\.tech/image}')
2930

3031
# Exclude stale CRDs from Helm chart, and apply the rest
3132
helm_crds, helm_non_crds = filter_yaml(
@@ -34,7 +35,7 @@ helm_crds, helm_non_crds = filter_yaml(
3435
name=operator_name,
3536
namespace="stackable-operators",
3637
set=[
37-
'image.repository=' + registry + '/' + operator_name,
38+
'secretOperator.image.repository=' + registry + '/' + operator_name,
3839
],
3940
),
4041
api_version = "^apiextensions\\.k8s\\.io/.*$",
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
apiVersion: apps/v1
3+
kind: Deployment
4+
metadata:
5+
name: {{ include "operator.fullname" . }}-controller
6+
labels:
7+
{{- include "operator.labels" . | nindent 4 }}
8+
spec:
9+
selector:
10+
matchLabels:
11+
{{- include "operator.selectorLabels" . | nindent 6 }}
12+
template:
13+
metadata:
14+
annotations:
15+
internal.stackable.tech/image: "{{ .Values.secretOperator.image.repository }}:{{ .Values.secretOperator.image.tag | default .Chart.AppVersion }}"
16+
{{- with .Values.podAnnotations }}
17+
{{- toYaml . | nindent 8 }}
18+
{{- end }}
19+
labels:
20+
{{- include "operator.selectorLabels" . | nindent 8 }}
21+
spec:
22+
{{- with .Values.imagePullSecrets }}
23+
imagePullSecrets:
24+
{{- toYaml . | nindent 8 }}
25+
{{- end }}
26+
# NOTE (@Techassi): Does it maybe make sense to have two different service accounts?
27+
serviceAccountName: {{ include "operator.fullname" . }}-serviceaccount
28+
securityContext:
29+
{{- toYaml .Values.podSecurityContext | nindent 8 }}
30+
containers:
31+
- name: {{ include "operator.appname" . }}-controller
32+
securityContext:
33+
{{- toYaml .Values.secretOperator.securityContext | nindent 12 }}
34+
image: "{{ .Values.secretOperator.image.repository }}:{{ .Values.secretOperator.image.tag | default .Chart.AppVersion }}"
35+
imagePullPolicy: {{ .Values.secretOperator.image.pullPolicy }}
36+
resources:
37+
{{ .Values.secretOperator.resources | toYaml | nindent 12 }}
38+
# The arguments passed to the command being run in the container. The final command will
39+
# look like `secret-operator run controller [OPTIONS]`. The controller needs to only run
40+
# once in a Kubernetes cluster and as such is deployed as a Deployment.
41+
args:
42+
- run
43+
- controller
44+
env:
45+
# The following env vars are passed as clap (think CLI) arguments to the operator.
46+
# They are picked up by clap using the structs defied in the operator.
47+
# (which is turn pulls in https://github.com/stackabletech/operator-rs/blob/main/crates/stackable-operator/src/cli.rs)
48+
# You can read there about the expected values and purposes.
49+
50+
# Sometimes products need to know the operator image, e.g. the opa-bundle-builder OPA
51+
# sidecar uses the operator image.
52+
- name: OPERATOR_IMAGE
53+
# Tilt can use annotations as image paths, but not env variables
54+
valueFrom:
55+
fieldRef:
56+
fieldPath: metadata.annotations['internal.stackable.tech/image']
57+
58+
# Namespace the operator Pod is running in, e.g. used to construct the conversion
59+
# webhook endpoint.
60+
- name: OPERATOR_NAMESPACE
61+
valueFrom:
62+
fieldRef:
63+
fieldPath: metadata.namespace
64+
65+
# The name of the Kubernetes Service that point to the operator Pod, e.g. used to
66+
# construct the conversion webhook endpoint.
67+
- name: OPERATOR_SERVICE_NAME
68+
value: {{ include "operator.fullname" . }}
69+
70+
# Operators need to know the node name they are running on, to e.g. discover the
71+
# Kubernetes domain name from the kubelet API.
72+
- name: KUBERNETES_NODE_NAME
73+
valueFrom:
74+
fieldRef:
75+
fieldPath: spec.nodeName
76+
77+
{{- if .Values.kubernetesClusterDomain }}
78+
- name: KUBERNETES_CLUSTER_DOMAIN
79+
value: {{ .Values.kubernetesClusterDomain | quote }}
80+
{{- end }}
81+
{{- include "telemetry.envVars" . | nindent 12 }}
82+
{{- with .Values.nodeSelector }}
83+
nodeSelector:
84+
{{- toYaml . | nindent 8 }}
85+
{{- end }}
86+
{{- with .Values.affinity }}
87+
affinity:
88+
{{- toYaml . | nindent 8 }}
89+
{{- end }}
90+
{{- with .Values.tolerations }}
91+
tolerations:
92+
{{- toYaml . | nindent 8 }}
93+
{{- end }}
94+
{{- with .Values.priorityClassName }}
95+
priorityClassName: {{ . }}
96+
{{- end }}

deploy/helm/secret-operator/templates/daemonset.yaml renamed to deploy/helm/secret-operator/templates/csi-server-daemonset.yaml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
apiVersion: apps/v1
33
kind: DaemonSet
44
metadata:
5-
name: {{ include "operator.fullname" . }}-daemonset
5+
name: {{ include "operator.fullname" . }}-csi-server
66
labels:
77
{{- include "operator.labels" . | nindent 4 }}
88
spec:
@@ -11,28 +11,36 @@ spec:
1111
{{- include "operator.selectorLabels" . | nindent 6 }}
1212
template:
1313
metadata:
14-
{{- with .Values.podAnnotations }}
1514
annotations:
16-
{{- toYaml . | nindent 8 }}
17-
{{- end }}
15+
internal.stackable.tech/image: "{{ .Values.secretOperator.image.repository }}:{{ .Values.secretOperator.image.tag | default .Chart.AppVersion }}"
16+
{{- with .Values.podAnnotations }}
17+
{{- toYaml . | nindent 8 }}
18+
{{- end }}
1819
labels:
1920
{{- include "operator.selectorLabels" . | nindent 8 }}
2021
spec:
2122
{{- with .Values.image.pullSecrets }}
2223
imagePullSecrets:
2324
{{- toYaml . | nindent 8 }}
2425
{{- end }}
26+
# NOTE (@Techassi): Does it maybe make sense to have two different service accounts?
2527
serviceAccountName: {{ include "operator.fullname" . }}-serviceaccount
2628
securityContext:
2729
{{- toYaml .Values.podSecurityContext | nindent 8 }}
2830
containers:
29-
- name: {{ include "operator.appname" . }}
31+
- name: {{ include "operator.appname" . }}-csi-server
3032
securityContext:
3133
{{- toYaml .Values.secretOperator.securityContext | nindent 12 }}
3234
image: "{{ .Values.secretOperator.image.repository }}:{{ .Values.secretOperator.image.tag | default .Chart.AppVersion }}"
3335
imagePullPolicy: {{ .Values.secretOperator.image.pullPolicy }}
3436
resources:
3537
{{ .Values.secretOperator.resources | toYaml | nindent 12 }}
38+
# The arguments passed to the command being run in the container. The final command will
39+
# look like `secret-operator run csi-server [OPTIONS]`. The CSI server needs to run on
40+
# every Kubernetes cluster node and as such is deployed as a DaemonSet.
41+
args:
42+
- run
43+
- csi-server
3644
env:
3745
# The following env vars are passed as clap (think CLI) arguments to the operator.
3846
# They are picked up by clap using the structs defied in the operator.

rust/operator-binary/src/main.rs

Lines changed: 105 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// This will need changes in our and upstream error types.
33
#![allow(clippy::result_large_err)]
44

5-
use std::{os::unix::prelude::FileTypeExt, path::PathBuf, pin::pin};
5+
use std::{os::unix::prelude::FileTypeExt, path::PathBuf};
66

77
use anyhow::Context;
88
use clap::Parser;
@@ -16,7 +16,9 @@ use grpc::csi::v1::{
1616
};
1717
use stackable_operator::{
1818
YamlSchema,
19-
cli::{CommonOptions, ProductOperatorRun},
19+
cli::{Command, CommonOptions, ProductOperatorRun},
20+
client::Client,
21+
namespace::WatchNamespace,
2022
shared::yaml::SerializeOptions,
2123
telemetry::Tracing,
2224
};
@@ -40,14 +42,33 @@ pub const OPERATOR_NAME: &str = "secrets.stackable.tech";
4042

4143
#[derive(clap::Parser)]
4244
#[clap(author, version)]
43-
struct Opts {
45+
struct Cli {
4446
#[clap(subcommand)]
45-
cmd: stackable_operator::cli::Command<SecretOperatorRun>,
47+
cmd: Command<SecretOperatorRun>,
4648
}
4749

4850
#[derive(clap::Parser)]
4951
struct SecretOperatorRun {
50-
#[clap(long, env)]
52+
/// The run mode in which this operator should run in.
53+
#[command(subcommand)]
54+
mode: RunMode,
55+
56+
#[clap(flatten)]
57+
common: ProductOperatorRun,
58+
}
59+
60+
#[derive(Debug, clap::Subcommand)]
61+
enum RunMode {
62+
/// Run the CSI server, one per Kubernetes cluster node.
63+
CsiServer(CsiServerArguments),
64+
65+
/// Run the controller, one per Kubernetes cluster.
66+
Controller,
67+
}
68+
69+
#[derive(Debug, clap::Args)]
70+
struct CsiServerArguments {
71+
#[arg(long, env)]
5172
csi_endpoint: PathBuf,
5273

5374
/// Unprivileged mode disables any features that require running secret-operator in a privileged container.
@@ -56,11 +77,8 @@ struct SecretOperatorRun {
5677
/// - Secret volumes will be stored on disk, rather than in a ramdisk
5778
///
5879
/// Unprivileged mode is EXPERIMENTAL and heavily discouraged, since it increases the risk of leaking secrets.
59-
#[clap(long, env)]
80+
#[arg(long, env)]
6081
privileged: bool,
61-
62-
#[clap(flatten)]
63-
common: ProductOperatorRun,
6482
}
6583

6684
mod built_info {
@@ -69,29 +87,28 @@ mod built_info {
6987

7088
#[tokio::main]
7189
async fn main() -> anyhow::Result<()> {
72-
let opts = Opts::parse();
73-
match opts.cmd {
74-
stackable_operator::cli::Command::Crd => {
90+
let cli = Cli::parse();
91+
92+
match cli.cmd {
93+
Command::Crd => {
7594
SecretClass::merged_crd(crd::SecretClassVersion::V1Alpha1)?
7695
.print_yaml_schema(built_info::PKG_VERSION, SerializeOptions::default())?;
7796
TrustStore::merged_crd(crd::TrustStoreVersion::V1Alpha1)?
7897
.print_yaml_schema(built_info::PKG_VERSION, SerializeOptions::default())?;
7998
}
80-
stackable_operator::cli::Command::Run(SecretOperatorRun {
81-
csi_endpoint,
82-
privileged,
83-
common:
84-
ProductOperatorRun {
85-
common:
86-
CommonOptions {
87-
telemetry,
88-
cluster_info,
89-
},
90-
product_config: _,
91-
watch_namespace,
92-
operator_environment: _,
93-
},
94-
}) => {
99+
Command::Run(SecretOperatorRun { common, mode }) => {
100+
let ProductOperatorRun {
101+
operator_environment: _,
102+
product_config: _,
103+
watch_namespace,
104+
common,
105+
} = common;
106+
107+
let CommonOptions {
108+
telemetry,
109+
cluster_info,
110+
} = common;
111+
95112
// NOTE (@NickLarsenNZ): Before stackable-telemetry was used:
96113
// - The console log level was set by `SECRET_PROVISIONER_LOG`, and is now `CONSOLE_LOG` (when using Tracing::pre_configured).
97114
// - The file log level was set by `SECRET_PROVISIONER_LOG`, and is now set via `FILE_LOG` (when using Tracing::pre_configured).
@@ -113,46 +130,70 @@ async fn main() -> anyhow::Result<()> {
113130
&cluster_info,
114131
)
115132
.await?;
116-
if csi_endpoint
117-
.symlink_metadata()
118-
.is_ok_and(|meta| meta.file_type().is_socket())
119-
{
120-
let _ = std::fs::remove_file(&csi_endpoint);
121-
}
122-
let mut sigterm = signal(SignalKind::terminate())?;
123-
let csi_server = pin!(
124-
Server::builder()
125-
.add_service(
126-
tonic_reflection::server::Builder::configure()
127-
.include_reflection_service(true)
128-
.register_encoded_file_descriptor_set(grpc::FILE_DESCRIPTOR_SET_BYTES)
129-
.build_v1()?,
130-
)
131-
.add_service(IdentityServer::new(SecretProvisionerIdentity))
132-
.add_service(ControllerServer::new(SecretProvisionerController {
133-
client: client.clone(),
134-
}))
135-
.add_service(NodeServer::new(SecretProvisionerNode {
136-
client: client.clone(),
137-
node_name: cluster_info.kubernetes_node_name.to_owned(),
133+
134+
match mode {
135+
RunMode::CsiServer(CsiServerArguments {
136+
csi_endpoint,
137+
privileged,
138+
}) => {
139+
run_csi_server(
140+
csi_endpoint,
141+
cluster_info.kubernetes_node_name,
138142
privileged,
139-
}))
140-
.serve_with_incoming_shutdown(
141-
UnixListenerStream::new(
142-
uds_bind_private(csi_endpoint)
143-
.context("failed to bind CSI listener")?,
144-
)
145-
.map_ok(TonicUnixStream),
146-
sigterm.recv().map(|_| ()),
143+
client,
147144
)
148-
);
149-
let truststore_controller =
150-
pin!(truststore_controller::start(&client, &watch_namespace).map(Ok));
151-
futures::future::select(csi_server, truststore_controller)
152-
.await
153-
.factor_first()
154-
.0?;
145+
.await?
146+
}
147+
RunMode::Controller => run_controller(watch_namespace, client).await,
148+
}
155149
}
156150
}
151+
157152
Ok(())
158153
}
154+
155+
async fn run_csi_server(
156+
csi_endpoint: PathBuf,
157+
node_name: String,
158+
privileged: bool,
159+
client: Client,
160+
) -> anyhow::Result<()> {
161+
if csi_endpoint
162+
.symlink_metadata()
163+
.is_ok_and(|meta| meta.file_type().is_socket())
164+
{
165+
let _ = std::fs::remove_file(&csi_endpoint);
166+
}
167+
168+
let mut sigterm = signal(SignalKind::terminate())?;
169+
170+
let csi_server = Server::builder()
171+
.add_service(
172+
tonic_reflection::server::Builder::configure()
173+
.include_reflection_service(true)
174+
.register_encoded_file_descriptor_set(grpc::FILE_DESCRIPTOR_SET_BYTES)
175+
.build_v1()?,
176+
)
177+
.add_service(IdentityServer::new(SecretProvisionerIdentity))
178+
.add_service(ControllerServer::new(SecretProvisionerController {
179+
client: client.clone(),
180+
}))
181+
.add_service(NodeServer::new(SecretProvisionerNode {
182+
privileged,
183+
node_name,
184+
client,
185+
}))
186+
.serve_with_incoming_shutdown(
187+
UnixListenerStream::new(
188+
uds_bind_private(csi_endpoint).context("failed to bind CSI listener")?,
189+
)
190+
.map_ok(TonicUnixStream),
191+
sigterm.recv().map(|_| ()),
192+
);
193+
194+
csi_server.await.context("failed to run the CSI server")
195+
}
196+
197+
async fn run_controller(watch_namespace: WatchNamespace, client: Client) {
198+
truststore_controller::start(client, &watch_namespace).await
199+
}

0 commit comments

Comments
 (0)