Skip to content

Commit 581ebad

Browse files
Techassisbernauer
andauthored
feat(ctl): Namespace separation and support skipping release installation (#79)
* Initial work for namespace separation * Apply suggestions Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.de> * Make namespaces configurable for all sub commands * Create namespace if needed * Add `--skip-release` flag * Remove stacklet alias * Apply suggestions Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.de> * Improve namespace::create_if_needed function * Create namespaces in operator and stack commands * Add support for supportedNamespaces in demos and stacks * Fix product namespace creation * Finalize support for supportedNamespaces * Add NamespaceError * Improve ns error handling * Slightly adjust error wording --------- Co-authored-by: Sebastian Bernauer <sebastian.bernauer@stackable.de>
1 parent 3016afd commit 581ebad

File tree

27 files changed

+2812
-450
lines changed

27 files changed

+2812
-450
lines changed

extra/completions/_stackablectl

Lines changed: 429 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extra/completions/stackablectl.bash

Lines changed: 1455 additions & 147 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extra/completions/stackablectl.fish

Lines changed: 288 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extra/man/stackablectl.1

Lines changed: 7 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/stackable-cockpit/src/constants.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
use std::time::Duration;
22

33
pub const REDACTED_PASSWORD: &str = "<redacted>";
4-
pub const PASSWORD_LEN: usize = 32;
4+
pub const PASSWORD_LENGTH: usize = 32;
55

6-
pub const DEFAULT_STACKABLE_NAMESPACE: &str = "stackable";
7-
pub const DEFAULT_NAMESPACE: &str = "default";
6+
pub const DEFAULT_OPERATOR_NAMESPACE: &str = "stackable-operators";
7+
// TODO (Techassi): Change this to "stackable" once we switch to this version.
8+
// Currently lots of demos can only run in the default namespace, so we have to
9+
// keep "default" here, until we switch the demos. We can't switch them right
10+
// now, as the old stackablectl would break.
11+
pub const DEFAULT_PRODUCT_NAMESPACE: &str = "default";
812

913
pub const DEFAULT_LOCAL_CLUSTER_NAME: &str = "stackable-data-platform";
1014

rust/stackable-cockpit/src/kube.rs

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ use std::string::FromUtf8Error;
33
use k8s_openapi::{
44
api::{
55
apps::v1::{Deployment, DeploymentCondition, StatefulSet, StatefulSetCondition},
6-
core::v1::{Secret, Service},
6+
core::v1::{Namespace, Secret, Service},
77
},
88
apimachinery::pkg::apis::meta::v1::Condition,
99
};
1010
use kube::{
11-
api::{ListParams, Patch, PatchParams},
12-
core::{DynamicObject, GroupVersionKind, ObjectList, TypeMeta},
11+
api::{ListParams, Patch, PatchParams, PostParams},
12+
core::{DynamicObject, GroupVersionKind, ObjectList, ObjectMeta, TypeMeta},
1313
discovery::Scope,
1414
Api, Client, Discovery, ResourceExt,
1515
};
@@ -22,6 +22,9 @@ use utoipa::ToSchema;
2222

2323
use crate::constants::REDACTED_PASSWORD;
2424

25+
pub type ListResult<T, E = KubeClientError> = Result<ObjectList<T>, E>;
26+
pub type Result<T, E = KubeClientError> = std::result::Result<T, E>;
27+
2528
#[derive(Debug, Snafu)]
2629
pub enum KubeClientError {
2730
#[snafu(display("kube error: {source}"))]
@@ -60,7 +63,7 @@ pub struct KubeClient {
6063
impl KubeClient {
6164
/// Tries to create a new default Kubernetes client and immediately runs
6265
/// a discovery.
63-
pub async fn new() -> Result<Self, KubeClientError> {
66+
pub async fn new() -> Result<Self> {
6467
let client = Client::try_default().await.context(KubeSnafu)?;
6568
let discovery = Discovery::new(client.clone())
6669
.run()
@@ -73,11 +76,7 @@ impl KubeClient {
7376
/// Deploys manifests defined the in raw `manifests` YAML string. This
7477
/// method will fail if it is unable to parse the manifests, unable to
7578
/// resolve GVKs or unable to patch the dynamic objects.
76-
pub async fn deploy_manifests(
77-
&self,
78-
manifests: &str,
79-
namespace: &str,
80-
) -> Result<(), KubeClientError> {
79+
pub async fn deploy_manifests(&self, manifests: &str, namespace: &str) -> Result<()> {
8180
for manifest in serde_yaml::Deserializer::from_str(manifests) {
8281
let mut object = DynamicObject::deserialize(manifest).context(YamlSnafu)?;
8382
let object_type = object.types.as_ref().ok_or(
@@ -115,7 +114,7 @@ impl KubeClient {
115114
Ok(())
116115
}
117116

118-
/// List objects by looking up a GVK via the discovery. It returns an
117+
/// Lists objects by looking up a GVK via the discovery. It returns an
119118
/// optional list of dynamic objects. The method returns [`Ok(None)`]
120119
/// if the client was unable to resolve the GVK. An error is returned
121120
/// when the client failed to list the objects.
@@ -146,15 +145,15 @@ impl KubeClient {
146145
Ok(Some(objects))
147146
}
148147

149-
/// List services by matching labels. The services can me matched by the
150-
/// product labels. [`ListParamsExt`] provides a utility function to
148+
/// Lists [`Service`]s by matching labels. The services can be matched by
149+
/// the product labels. [`ListParamsExt`] provides a utility function to
151150
/// create [`ListParams`] based on a product name and optional instance
152151
/// name.
153152
pub async fn list_services(
154153
&self,
155154
namespace: Option<&str>,
156155
list_params: &ListParams,
157-
) -> Result<ObjectList<Service>, KubeClientError> {
156+
) -> ListResult<Service> {
158157
let service_api: Api<Service> = match namespace {
159158
Some(namespace) => Api::namespaced(self.client.clone(), namespace),
160159
None => Api::all(self.client.clone()),
@@ -174,7 +173,7 @@ impl KubeClient {
174173
secret_namespace: &str,
175174
username_key: &str,
176175
password_key: Option<&str>,
177-
) -> Result<Option<(String, String)>, KubeClientError> {
176+
) -> Result<Option<(String, String)>> {
178177
let secret_api: Api<Secret> = Api::namespaced(self.client.clone(), secret_namespace);
179178

180179
let secret = secret_api.get(secret_name).await.context(KubeSnafu)?;
@@ -200,11 +199,14 @@ impl KubeClient {
200199
Ok(Some((username, password)))
201200
}
202201

202+
/// Lists [`Deployment`]s by matching labels. The services can be matched
203+
/// by the app labels. [`ListParamsExt`] provides a utility function to
204+
/// create [`ListParams`] based on a app name and other labels.
203205
pub async fn list_deployments(
204206
&self,
205207
namespace: Option<&str>,
206208
list_params: &ListParams,
207-
) -> Result<ObjectList<Deployment>, KubeClientError> {
209+
) -> ListResult<Deployment> {
208210
let deployment_api: Api<Deployment> = match namespace {
209211
Some(namespace) => Api::namespaced(self.client.clone(), namespace),
210212
None => Api::all(self.client.clone()),
@@ -215,11 +217,14 @@ impl KubeClient {
215217
Ok(deployments)
216218
}
217219

220+
/// Lists [`StatefulSet`]s by matching labels. The services can be matched
221+
/// by the app labels. [`ListParamsExt`] provides a utility function to
222+
/// create [`ListParams`] based on a app name and other labels.
218223
pub async fn list_stateful_sets(
219224
&self,
220225
namespace: Option<&str>,
221226
list_params: &ListParams,
222-
) -> Result<ObjectList<StatefulSet>, KubeClientError> {
227+
) -> ListResult<StatefulSet> {
223228
let stateful_set_api: Api<StatefulSet> = match namespace {
224229
Some(namespace) => Api::namespaced(self.client.clone(), namespace),
225230
None => Api::all(self.client.clone()),
@@ -233,7 +238,48 @@ impl KubeClient {
233238
Ok(stateful_sets)
234239
}
235240

236-
/// Extracts the GVK from [`TypeMeta`].
241+
/// Returns a [`Namespace`] identified by name. If this namespace doesn't
242+
/// exist, this method returns [`None`].
243+
pub async fn get_namespace(&self, name: &str) -> Result<Option<Namespace>> {
244+
let namespace_api: Api<Namespace> = Api::all(self.client.clone());
245+
namespace_api.get_opt(name).await.context(KubeSnafu)
246+
}
247+
248+
/// Creates a [`Namespace`] with `name` in the cluster. This method will
249+
/// return an error if the namespace already exists. Instead of using this
250+
/// method directly, it is advised to use [`namespace::create_if_needed`][1]
251+
/// instead.
252+
///
253+
/// [1]: crate::platform::namespace
254+
pub async fn create_namespace(&self, name: String) -> Result<()> {
255+
let namespace_api: Api<Namespace> = Api::all(self.client.clone());
256+
namespace_api
257+
.create(
258+
&PostParams::default(),
259+
&Namespace {
260+
metadata: ObjectMeta {
261+
name: Some(name),
262+
..Default::default()
263+
},
264+
..Default::default()
265+
},
266+
)
267+
.await
268+
.context(KubeSnafu)?;
269+
270+
Ok(())
271+
}
272+
273+
/// Creates a [`Namespace`] only if not already present in the current cluster.
274+
pub async fn create_namespace_if_needed(&self, name: String) -> Result<()> {
275+
if self.get_namespace(&name).await?.is_none() {
276+
self.create_namespace(name).await?
277+
}
278+
279+
Ok(())
280+
}
281+
282+
/// Extracts the [`GroupVersionKind`] from [`TypeMeta`].
237283
fn gvk_of_typemeta(type_meta: &TypeMeta) -> GroupVersionKind {
238284
match type_meta.api_version.split_once('/') {
239285
Some((group, version)) => GroupVersionKind::gvk(group, version, &type_meta.kind),

rust/stackable-cockpit/src/platform/demo/spec.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
use serde::{Deserialize, Serialize};
2+
use snafu::{ResultExt, Snafu};
23

34
#[cfg(feature = "openapi")]
45
use utoipa::ToSchema;
56

67
use crate::{
78
common::ManifestSpec,
9+
platform::{
10+
release::ReleaseList,
11+
stack::{StackError, StackList},
12+
},
813
utils::params::{Parameter, RawParameter, RawParameterParseError},
14+
xfer::FileTransferClient,
915
};
1016

1117
pub type RawDemoParameterParseError = RawParameterParseError;
@@ -24,6 +30,11 @@ pub struct DemoSpecV2 {
2430
#[serde(skip_serializing_if = "Option::is_none")]
2531
pub documentation: Option<String>,
2632

33+
/// Supported namespaces this demo can run in. An empty list indicates that
34+
/// the demo can run in any namespace.
35+
#[serde(default)]
36+
pub supported_namespaces: Vec<String>,
37+
2738
/// The name of the underlying stack
2839
#[serde(rename = "stackableStack")]
2940
pub stack: String,
@@ -40,3 +51,82 @@ pub struct DemoSpecV2 {
4051
#[serde(default)]
4152
pub parameters: Vec<Parameter>,
4253
}
54+
55+
#[derive(Debug, Snafu)]
56+
pub enum DemoError {
57+
#[snafu(display("no stack with name '{name}'"))]
58+
NoSuchStack { name: String },
59+
60+
#[snafu(display("stack error"))]
61+
StackError { source: StackError },
62+
63+
#[snafu(display("cannot install demo in namespace '{requested}', only '{}' supported", supported.join(", ")))]
64+
UnsupportedNamespace {
65+
requested: String,
66+
supported: Vec<String>,
67+
},
68+
}
69+
70+
impl DemoSpecV2 {
71+
#[allow(clippy::too_many_arguments)]
72+
pub async fn install(
73+
&self,
74+
stack_list: StackList,
75+
release_list: ReleaseList,
76+
operator_namespace: &str,
77+
product_namespace: &str,
78+
stack_parameters: &[String],
79+
demo_parameters: &[String],
80+
transfer_client: &FileTransferClient,
81+
skip_release: bool,
82+
) -> Result<(), DemoError> {
83+
// Returns an error if the demo doesn't support to be installed in the
84+
// requested namespace
85+
if !self.supports_namespace(product_namespace) {
86+
return Err(DemoError::UnsupportedNamespace {
87+
requested: product_namespace.to_string(),
88+
supported: self.supported_namespaces.clone(),
89+
});
90+
}
91+
92+
// Get the stack spec based on the name defined in the demo spec
93+
let stack_spec = stack_list.get(&self.stack).ok_or(DemoError::NoSuchStack {
94+
name: self.stack.clone(),
95+
})?;
96+
97+
// Install the stack
98+
stack_spec
99+
.install(
100+
release_list,
101+
operator_namespace,
102+
product_namespace,
103+
skip_release,
104+
)
105+
.context(StackSnafu)?;
106+
107+
// Install stack manifests
108+
stack_spec
109+
.install_stack_manifests(stack_parameters, product_namespace, transfer_client)
110+
.await
111+
.context(StackSnafu)?;
112+
113+
// Install demo manifests
114+
stack_spec
115+
.install_demo_manifests(
116+
&self.manifests,
117+
&self.parameters,
118+
demo_parameters,
119+
product_namespace,
120+
transfer_client,
121+
)
122+
.await
123+
.context(StackSnafu)?;
124+
125+
Ok(())
126+
}
127+
128+
fn supports_namespace(&self, namespace: impl Into<String>) -> bool {
129+
self.supported_namespaces.is_empty()
130+
|| self.supported_namespaces.contains(&namespace.into())
131+
}
132+
}

rust/stackable-cockpit/src/platform/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod demo;
2+
pub mod namespace;
23
pub mod operator;
34
pub mod product;
45
pub mod release;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use snafu::{ResultExt, Snafu};
2+
3+
use crate::kube::{KubeClient, KubeClientError};
4+
5+
#[derive(Debug, Snafu)]
6+
pub enum NamespaceError {
7+
#[snafu(display("kubernetes client error"))]
8+
KubeClientError { source: KubeClientError },
9+
10+
#[snafu(display("permission denied - try to create the namespace manually or choose an already existing one to which you have access to"))]
11+
PermissionDenied,
12+
}
13+
14+
/// Creates a namespace with `name` if needed (not already present in the
15+
/// cluster).
16+
pub async fn create_if_needed(name: String) -> Result<(), NamespaceError> {
17+
let client = KubeClient::new().await.context(KubeClientSnafu)?;
18+
client
19+
.create_namespace_if_needed(name)
20+
.await
21+
.map_err(|err| match err {
22+
KubeClientError::KubeError { source } => match source {
23+
kube::Error::Api(err) if err.code == 401 => NamespaceError::PermissionDenied,
24+
_ => NamespaceError::KubeClientError {
25+
source: KubeClientError::KubeError { source },
26+
},
27+
},
28+
_ => NamespaceError::KubeClientError { source: err },
29+
})
30+
}

0 commit comments

Comments
 (0)