Skip to content

Commit 1852366

Browse files
authored
Merge branch 'main' into refactor/remove-derivative
2 parents 59f0069 + 41c439c commit 1852366

File tree

62 files changed

+3870
-2637
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3870
-2637
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ env:
1717
CARGO_TERM_COLOR: always
1818
CARGO_INCREMENTAL: '0'
1919
CARGO_PROFILE_DEV_DEBUG: '0'
20-
RUST_TOOLCHAIN_VERSION: "1.81.0"
20+
RUST_TOOLCHAIN_VERSION: "1.82.0"
2121
RUSTFLAGS: "-D warnings"
2222
RUSTDOCFLAGS: "-D warnings"
2323
RUST_LOG: "info"

.github/workflows/pr_pre-commit.yaml

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66

77
env:
88
CARGO_TERM_COLOR: always
9-
RUST_TOOLCHAIN_VERSION: "1.81.0"
9+
RUST_TOOLCHAIN_VERSION: "1.82.0"
1010
HADOLINT_VERSION: "v1.17.6"
1111

1212
jobs:
@@ -16,29 +16,10 @@ jobs:
1616
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
1717
with:
1818
fetch-depth: 0
19-
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
19+
- uses: stackabletech/actions/run-pre-commit@9bd13255f286e4b7a654617268abe1b2f37c3e0a # v0.3.0
2020
with:
21-
python-version: '3.12'
22-
- uses: dtolnay/rust-toolchain@master
23-
with:
24-
toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }}
25-
components: rustfmt,clippy
26-
- name: Setup Hadolint
27-
shell: bash
28-
run: |
29-
set -euo pipefail
30-
31-
LOCATION_DIR="$HOME/.local/bin"
32-
LOCATION_BIN="$LOCATION_DIR/hadolint"
33-
34-
SYSTEM=$(uname -s)
35-
ARCH=$(uname -m)
36-
37-
mkdir -p "$LOCATION_DIR"
38-
curl -sL -o "${LOCATION_BIN}" "https://github.com/hadolint/hadolint/releases/download/${{ env.HADOLINT_VERSION }}/hadolint-$SYSTEM-$ARCH"
39-
chmod 700 "${LOCATION_BIN}"
40-
41-
echo "$LOCATION_DIR" >> "$GITHUB_PATH"
42-
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
43-
with:
44-
extra_args: "--from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }}"
21+
rust: ${{ env.RUST_TOOLCHAIN_VERSION }}
22+
# rust-src is required for trybuild stderr output comparison to work
23+
# for our cases.
24+
# See: https://github.com/dtolnay/trybuild/issues/236#issuecomment-1620950759
25+
rust-components: rustfmt,clippy,rust-src

Cargo.lock

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/stackable-operator/CHANGELOG.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,27 @@ All notable changes to this project will be documented in this file.
1010

1111
[#907]: https://github.com/stackabletech/operator-rs/pull/907
1212

13+
## [0.82.0] - 2024-11-23
14+
15+
### Fixed
16+
17+
- Fixed URL handling related to OIDC and `rootPath` with and without trailing slashes. Also added a bunch of tests ([#910]).
18+
19+
### Changed
20+
21+
- BREAKING: Made `DEFAULT_OIDC_WELLKNOWN_PATH` private. Use `AuthenticationProvider::well_known_config_url` instead ([#910]).
22+
- BREAKING: Changed visibility of `commons::rbac::service_account_name` and `commons::rbac::role_binding_name` to
23+
private, as these functions should not be called directly by the operators. This is likely to result in naming conflicts
24+
as the result is completely dependent on what is passed to this function. Operators should instead rely on the roleBinding
25+
and serviceAccount objects created by `commons::rbac::build_rbac_resources` and retrieve the name from the returned
26+
objects if they need it ([#909]).
27+
- Changed the names of the objects that are returned from `commons::rbac::build_rbac_resources` to not rely solely on the product
28+
they refer to (e.g. "nifi-rolebinding") but instead include the name of the resource to be unique per cluster
29+
(e.g. simple-nifi-rolebinding) ([#909]).
30+
31+
[#909]: https://github.com/stackabletech/operator-rs/pull/909
32+
[#910]: https://github.com/stackabletech/operator-rs/pull/910
33+
1334
## [0.81.0] - 2024-11-05
1435

1536
### Added
@@ -18,7 +39,7 @@ All notable changes to this project will be documented in this file.
1839

1940
### Changed
2041

21-
- BREAKING: Split `ListenerClass.spec.preferred_address_type` into a new `PreferredAddressType` type. Use `resolve_preferred_address_type()` to access the `AddressType` as before. ([#903])
42+
- BREAKING: Split `ListenerClass.spec.preferred_address_type` into a new `PreferredAddressType` type. Use `resolve_preferred_address_type()` to access the `AddressType` as before ([#903]).
2243

2344
[#903]: https://github.com/stackabletech/operator-rs/pull/903
2445

crates/stackable-operator/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "stackable-operator"
33
description = "Stackable Operator Framework"
4-
version = "0.81.0"
4+
version = "0.82.0"
55
authors.workspace = true
66
license.workspace = true
77
edition.workspace = true

crates/stackable-operator/src/commons/authentication/oidc.rs

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@ use crate::commons::{
1717

1818
pub type Result<T, E = Error> = std::result::Result<T, E>;
1919

20-
pub const DEFAULT_OIDC_WELLKNOWN_PATH: &str = ".well-known/openid-configuration";
2120
pub const CLIENT_ID_SECRET_KEY: &str = "clientId";
2221
pub const CLIENT_SECRET_SECRET_KEY: &str = "clientSecret";
2322

23+
/// Do *not* use this for [`Url::join`], as the leading slash will erase the existing path!
24+
const DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH: &str = "/.well-known/openid-configuration";
25+
2426
#[derive(Debug, PartialEq, Snafu)]
2527
pub enum Error {
2628
#[snafu(display("failed to parse OIDC endpoint url"))]
2729
ParseOidcEndpointUrl { source: ParseError },
2830

2931
#[snafu(display(
30-
"failed to set OIDC endpoint scheme '{scheme}' for endpoint url '{endpoint}'"
32+
"failed to set OIDC endpoint scheme '{scheme}' for endpoint url \"{endpoint}\""
3133
))]
3234
SetOidcEndpointScheme { endpoint: Url, scheme: String },
3335
}
@@ -111,10 +113,10 @@ impl AuthenticationProvider {
111113
}
112114
}
113115

114-
/// Returns the OIDC endpoint [`Url`]. To append the default OIDC well-known
115-
/// configuration path, use `url.join()`. This module provides the default
116-
/// path at [`DEFAULT_OIDC_WELLKNOWN_PATH`].
117-
pub fn endpoint_url(&self) -> Result<Url> {
116+
/// Returns the OIDC base [`Url`] without any path segments.
117+
///
118+
/// The base url only contains the scheme, the host, and an optional port.
119+
fn base_url(&self) -> Result<Url> {
118120
let mut url = Url::parse(&format!(
119121
"http://{host}:{port}",
120122
host = self.hostname.as_url_host(),
@@ -132,7 +134,37 @@ impl AuthenticationProvider {
132134
})?;
133135
}
134136

135-
url.set_path(&self.root_path);
137+
Ok(url)
138+
}
139+
140+
/// Returns the OIDC endpoint [`Url`] without a trailing slash.
141+
///
142+
/// To retrieve the well-known OIDC configuration url, please use [`Self::well_known_config_url`].
143+
pub fn endpoint_url(&self) -> Result<Url> {
144+
let mut url = self.base_url()?;
145+
// Some tools can not cope with a trailing slash, so let's remove that
146+
url.set_path(self.root_path.trim_end_matches('/'));
147+
Ok(url)
148+
}
149+
150+
/// Returns the well-known OIDC configuration [`Url`] without a trailing slash.
151+
///
152+
/// The returned url is a combination of [`Self::endpoint_url`] joined with
153+
/// the well-known OIDC configuration path `DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH`.
154+
pub fn well_known_config_url(&self) -> Result<Url> {
155+
let mut url = self.base_url()?;
156+
157+
// Taken from https://docs.rs/url/latest/url/struct.Url.html#method.join:
158+
// A trailing slash is significant. Without it, the last path component is considered to be
159+
// a “file” name to be removed to get at the “directory” that is used as the base.
160+
//
161+
// Because of that behavior, we first need to make sure that the root path doesn't contain
162+
// any trailing slashes to finally append the well-known config path to the url. The path
163+
// already contains a prefixed slash.
164+
let mut root_path_with_trailing_slash = self.root_path.trim_end_matches('/').to_string();
165+
root_path_with_trailing_slash.push_str(DEFAULT_WELLKNOWN_OIDC_CONFIG_PATH);
166+
url.set_path(&root_path_with_trailing_slash);
167+
136168
Ok(url)
137169
}
138170

@@ -246,6 +278,8 @@ pub struct ClientAuthenticationOptions<T = ()> {
246278

247279
#[cfg(test)]
248280
mod test {
281+
use rstest::rstest;
282+
249283
use super::*;
250284

251285
#[test]
@@ -310,12 +344,8 @@ mod test {
310344
.unwrap();
311345

312346
assert_eq!(
313-
oidc.endpoint_url()
314-
.unwrap()
315-
.join(DEFAULT_OIDC_WELLKNOWN_PATH)
316-
.unwrap()
317-
.as_str(),
318-
"https://my.keycloak.server/.well-known/openid-configuration"
347+
oidc.endpoint_url().unwrap().as_str(),
348+
"https://my.keycloak.server/"
319349
);
320350
}
321351

@@ -338,6 +368,70 @@ mod test {
338368
);
339369
}
340370

371+
#[rstest]
372+
#[case("/", "http://my.keycloak.server:1234/")]
373+
#[case("/realms/sdp", "http://my.keycloak.server:1234/realms/sdp")]
374+
#[case("/realms/sdp/", "http://my.keycloak.server:1234/realms/sdp")]
375+
#[case("/realms/sdp//////", "http://my.keycloak.server:1234/realms/sdp")]
376+
#[case(
377+
"/realms/my/realm/with/slashes//////",
378+
"http://my.keycloak.server:1234/realms/my/realm/with/slashes"
379+
)]
380+
fn root_path_endpoint_url(#[case] root_path: String, #[case] expected_endpoint_url: &str) {
381+
let oidc = serde_yaml::from_str::<AuthenticationProvider>(&format!(
382+
"
383+
hostname: my.keycloak.server
384+
port: 1234
385+
rootPath: {root_path}
386+
scopes: [openid]
387+
principalClaim: preferred_username
388+
"
389+
))
390+
.unwrap();
391+
392+
assert_eq!(oidc.endpoint_url().unwrap().as_str(), expected_endpoint_url);
393+
}
394+
395+
#[rstest]
396+
#[case("/", "https://my.keycloak.server/.well-known/openid-configuration")]
397+
#[case(
398+
"/realms/sdp",
399+
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
400+
)]
401+
#[case(
402+
"/realms/sdp/",
403+
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
404+
)]
405+
#[case(
406+
"/realms/sdp//////",
407+
"https://my.keycloak.server/realms/sdp/.well-known/openid-configuration"
408+
)]
409+
#[case(
410+
"/realms/my/realm/with/slashes//////",
411+
"https://my.keycloak.server/realms/my/realm/with/slashes/.well-known/openid-configuration"
412+
)]
413+
fn root_path_well_known_url(#[case] root_path: String, #[case] expected_well_known_url: &str) {
414+
let oidc = serde_yaml::from_str::<AuthenticationProvider>(&format!(
415+
"
416+
hostname: my.keycloak.server
417+
rootPath: {root_path}
418+
scopes: [openid]
419+
principalClaim: preferred_username
420+
tls:
421+
verification:
422+
server:
423+
caCert:
424+
webPki: {{}}
425+
"
426+
))
427+
.unwrap();
428+
429+
assert_eq!(
430+
oidc.well_known_config_url().unwrap().as_str(),
431+
expected_well_known_url
432+
);
433+
}
434+
341435
#[test]
342436
fn client_env_vars() {
343437
let secret_name = "my-keycloak-client";

crates/stackable-operator/src/commons/rbac.rs

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,25 @@ pub enum Error {
2828
}
2929

3030
/// Build RBAC objects for the product workloads.
31-
/// The `rbac_prefix` is meant to be the product name, for example: zookeeper, airflow, etc.
32-
/// and it is a assumed that a ClusterRole named `{rbac_prefix}-clusterrole` exists.
31+
/// The `product_name` is meant to be the product name, for example: zookeeper, airflow, etc.
32+
/// and it is a assumed that a ClusterRole named `{product_name}-clusterrole` exists.
33+
3334
pub fn build_rbac_resources<T: Clone + Resource<DynamicType = ()>>(
3435
resource: &T,
35-
rbac_prefix: &str,
36+
// 'product_name' is not used to build the names of the serviceAccount and roleBinding objects,
37+
// as this caused problems with multiple clusters of the same product within the same namespace
38+
// see <https://stackable.atlassian.net/browse/SUP-148> for more details.
39+
// Instead the names for these objects are created by reading the name from the cluster object
40+
// and appending [-rolebinding|-serviceaccount] to create unique names instead of using the
41+
// same objects for multiple clusters.
42+
product_name: &str,
3643
labels: Labels,
3744
) -> Result<(ServiceAccount, RoleBinding)> {
38-
let sa_name = service_account_name(rbac_prefix);
45+
let sa_name = service_account_name(&resource.name_any());
46+
// We add the legacy serviceAccount name to the binding here for at least one
47+
// release cycle, so that the switchover during the upgrade can be smoother.
48+
// To be removed in v24.3+1.
49+
let legacy_sa_name = service_account_name(product_name);
3950
let service_account = ServiceAccount {
4051
metadata: ObjectMetaBuilder::new()
4152
.name_and_namespace(resource)
@@ -52,7 +63,7 @@ pub fn build_rbac_resources<T: Clone + Resource<DynamicType = ()>>(
5263
let role_binding = RoleBinding {
5364
metadata: ObjectMetaBuilder::new()
5465
.name_and_namespace(resource)
55-
.name(role_binding_name(rbac_prefix))
66+
.name(role_binding_name(&resource.name_any()))
5667
.ownerreference_from_resource(resource, None, Some(true))
5768
.context(RoleBindingOwnerReferenceFromResourceSnafu {
5869
name: resource.name_any(),
@@ -61,29 +72,45 @@ pub fn build_rbac_resources<T: Clone + Resource<DynamicType = ()>>(
6172
.build(),
6273
role_ref: RoleRef {
6374
kind: "ClusterRole".to_string(),
64-
name: format!("{rbac_prefix}-clusterrole"),
75+
name: format!("{product_name}-clusterrole"),
6576
api_group: "rbac.authorization.k8s.io".to_string(),
6677
},
67-
subjects: Some(vec![Subject {
68-
kind: "ServiceAccount".to_string(),
69-
name: sa_name,
70-
namespace: resource.namespace(),
71-
..Subject::default()
72-
}]),
78+
subjects: Some(vec![
79+
Subject {
80+
kind: "ServiceAccount".to_string(),
81+
name: sa_name,
82+
namespace: resource.namespace(),
83+
..Subject::default()
84+
},
85+
// We add the legacy serviceAccount name to the binding here for at least one
86+
// release cycle, so that the switchover during the upgrade can be smoother.
87+
Subject {
88+
kind: "ServiceAccount".to_string(),
89+
name: legacy_sa_name,
90+
namespace: resource.namespace(),
91+
..Subject::default()
92+
},
93+
]),
7394
};
7495

7596
Ok((service_account, role_binding))
7697
}
7798

7899
/// Generate the service account name.
79100
/// The `rbac_prefix` is meant to be the product name, for example: zookeeper, airflow, etc.
80-
pub fn service_account_name(rbac_prefix: &str) -> String {
101+
/// This is private because operators should not use this function to calculate names for
102+
/// serviceAccount objects, but rather read the name from the objects returned by
103+
/// `build_rbac_resources` if they need the name.
104+
fn service_account_name(rbac_prefix: &str) -> String {
81105
format!("{rbac_prefix}-serviceaccount")
82106
}
83107

84108
/// Generate the role binding name.
85109
/// The `rbac_prefix` is meant to be the product name, for example: zookeeper, airflow, etc.
86-
pub fn role_binding_name(rbac_prefix: &str) -> String {
110+
/// This is private because operators should not use this function to calculate names for
111+
/// roleBinding objects, but rather read the name from the objects returned by
112+
/// `build_rbac_resources` if they need the name.
113+
fn role_binding_name(rbac_prefix: &str) -> String {
87114
format!("{rbac_prefix}-rolebinding")
88115
}
89116

@@ -130,7 +157,7 @@ mod tests {
130157
build_rbac_resources(&cluster, RESOURCE_NAME, Labels::new()).unwrap();
131158

132159
assert_eq!(
133-
Some(service_account_name(RESOURCE_NAME)),
160+
Some(service_account_name(CLUSTER_NAME)),
134161
rbac_sa.metadata.name,
135162
"service account does not match"
136163
);
@@ -141,7 +168,7 @@ mod tests {
141168
);
142169

143170
assert_eq!(
144-
Some(role_binding_name(RESOURCE_NAME)),
171+
Some(role_binding_name(CLUSTER_NAME)),
145172
rbac_rolebinding.metadata.name,
146173
"rolebinding does not match"
147174
);

0 commit comments

Comments
 (0)