Skip to content
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

CORE-7343 License: add fallback license env var #23583

Merged
merged 3 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/v/cluster/feature_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -309,17 +309,38 @@ void feature_manager::verify_enterprise_license() {
}

const auto& license = _feature_table.local().get_license();
auto license_missing_or_expired = !license || license->is_expired();
std::optional<security::license> fallback_license = std::nullopt;
auto fallback_license_str = std::getenv(
"REDPANDA_FALLBACK_ENTERPRISE_LICENSE");
if (fallback_license_str != nullptr) {
try {
fallback_license.emplace(
security::make_license(fallback_license_str));
} catch (const security::license_exception& e) {
// Log the error and continue without a fallback license
vlog(
clusterlog.warn,
"Failed to parse fallback license: {}",
e.what());
}
}

auto invalid = [](const std::optional<security::license>& license) {
return !license || license->is_expired();
};
auto license_missing_or_expired = invalid(license)
&& invalid(fallback_license);
auto enterprise_features = report_enterprise_features();

vlog(
clusterlog.info,
"Verifying enterprise license: active_version={}, latest_version={}, "
"enterprise_features=[{}], license_missing_or_expired={}",
"enterprise_features=[{}], license_missing_or_expired={}{}",
_feature_table.local().get_active_version(),
_feature_table.local().get_latest_logical_version(),
enterprise_features.enabled(),
license_missing_or_expired);
license_missing_or_expired,
fallback_license ? " (detected fallback license)" : "");

if (enterprise_features.any() && license_missing_or_expired) {
throw std::runtime_error{fmt::format(
Expand Down
8 changes: 4 additions & 4 deletions src/v/security/license.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ struct license_components {
ss::sstring signature;
};

static license_components parse_license(const ss::sstring& license) {
static license_components parse_license(std::string_view license) {
static constexpr auto signature_delimiter = ".";
const auto itr = license.find(signature_delimiter);
if (itr == ss::sstring::npos) {
Expand All @@ -97,7 +97,7 @@ static license_components parse_license(const ss::sstring& license) {
/// done so that it can have a utf-8 interpretation so the license file
/// doesn't have to be in binary format
return license_components{
.data = license.substr(0, itr),
.data = ss::sstring{license.substr(0, itr)},
.signature = base64_to_string(
license.substr(itr + strlen(signature_delimiter)))};
}
Expand Down Expand Up @@ -151,7 +151,7 @@ static void parse_data_section(license& lc, const json::Document& doc) {
lc.type = integer_to_license_type(doc.FindMember("type")->value.GetInt());
}

static ss::sstring calculate_sha256_checksum(const ss::sstring& raw_license) {
static ss::sstring calculate_sha256_checksum(std::string_view raw_license) {
bytes checksum;
hash_sha256 h;
h.update(raw_license);
Expand All @@ -161,7 +161,7 @@ static ss::sstring calculate_sha256_checksum(const ss::sstring& raw_license) {
return to_hex(checksum);
}

license make_license(const ss::sstring& raw_license) {
license make_license(std::string_view raw_license) {
try {
license lc;
auto components = parse_license(raw_license);
Expand Down
2 changes: 1 addition & 1 deletion src/v/security/license.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ struct license
/// failed, reasons could be:
/// 1. Malformed license
/// 2. Invalid license
license make_license(const ss::sstring& raw_license);
license make_license(std::string_view raw_license);

} // namespace security

Expand Down
6 changes: 1 addition & 5 deletions tests/rptest/services/redpanda.py
Original file line number Diff line number Diff line change
Expand Up @@ -5337,11 +5337,7 @@ def wait_node_add_rebalance_finished(self,
def install_license(self):
"""Install a sample Enterprise License for testing Enterprise features during upgrades"""
self.logger.debug("Installing an Enterprise License")
license = sample_license()
assert license, (
"No enterprise license found in the environment variable. "
"Please follow these instructions to get a sample license for local development: "
"https://redpandadata.atlassian.net/l/cp/4eeNEgZW")
license = sample_license(assert_exists=True)
assert self._admin.put_license(license).status_code == 200, \
"Configuring the Enterprise license failed (required for feature upgrades)"

Expand Down
49 changes: 49 additions & 0 deletions tests/rptest/tests/license_enforcement_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from rptest.services.redpanda import LoggingConfig
from rptest.tests.redpanda_test import RedpandaTest
from rptest.services.redpanda_installer import RedpandaInstaller
from rptest.utils.rpenv import sample_license


class LicenseEnforcementTest(RedpandaTest):
Expand Down Expand Up @@ -101,6 +102,54 @@ def test_license_enforcement(self, clean_node_before_recovery,
auto_assign_node_id=True,
omit_seeds_on_idx_one=False)

@cluster(num_nodes=5, log_allow_list=LOG_ALLOW_LIST)
@matrix(clean_node_before_upgrade=[False, True])
def test_escape_hatch_license_variable(self, clean_node_before_upgrade):
installer = self.redpanda._installer

prev_version = installer.highest_from_prior_feature_version(
RedpandaInstaller.HEAD)
latest_version = installer.head_version()
self.logger.info(
f"Testing with versions: {prev_version=} {latest_version=}")

self.logger.info(f"Starting all nodes with version: {prev_version}")
installer.install(self.redpanda.nodes, prev_version)
self.redpanda.start(nodes=self.redpanda.nodes,
omit_seeds_on_idx_one=False)

self.redpanda.wait_until(self.redpanda.healthy,
timeout_sec=60,
backoff_sec=1,
err_msg="The cluster hasn't stabilized")

self.logger.info(f"Enabling an enterprise feature")
self.rpk.create_role("XYZ")

first_upgraded = self.redpanda.nodes[0]
self.logger.info(
f"Upgrading node {first_upgraded} with an license injected via the environment variable escape hatch expecting it to succeed"
)
installer.install([first_upgraded], latest_version)
self.redpanda.stop_node(first_upgraded)

if clean_node_before_upgrade:
self.logger.info(f"Cleaning node {first_upgraded}")
self.redpanda.remove_local_data(first_upgraded)

license = sample_license(assert_exists=True)
self.redpanda.set_environment(
{'REDPANDA_FALLBACK_ENTERPRISE_LICENSE': license})

self.redpanda.start_node(first_upgraded,
auto_assign_node_id=True,
omit_seeds_on_idx_one=False)

self.redpanda.wait_until(self.redpanda.healthy,
timeout_sec=60,
backoff_sec=1,
err_msg="The cluster hasn't stabilized")

@cluster(num_nodes=5)
@matrix(root_driven_bootstrap=[False, True])
def test_enterprise_cluster_bootstrap(self, root_driven_bootstrap):
Expand Down
6 changes: 5 additions & 1 deletion tests/rptest/utils/rpenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import os


def sample_license():
def sample_license(assert_exists=False):
"""
Returns the sample license from the env if it exists, asserts if its
missing and the environment is CI
Expand All @@ -21,6 +21,10 @@ def sample_license():
if license is None:
is_ci = os.environ.get("CI", "false")
assert is_ci == "false"
assert not assert_exists, (
"No enterprise license found in the environment variable. "
"Please follow these instructions to get a sample license for local development: "
"https://redpandadata.atlassian.net/l/cp/4eeNEgZW")
return None
return license

Expand Down
Loading