Skip to content

Commit

Permalink
Add support for custom CC API users
Browse files Browse the repository at this point in the history
Signed-off-by: Kyle Liberti <kliberti@redhat.com>
  • Loading branch information
kyguy committed Jun 5, 2024
1 parent 7221769 commit ab23356
Show file tree
Hide file tree
Showing 20 changed files with 821 additions and 277 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Strimzi authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
package io.strimzi.api.kafka.model.kafka.cruisecontrol;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.strimzi.api.kafka.model.common.Constants;
import io.strimzi.api.kafka.model.common.PasswordSource;
import io.strimzi.api.kafka.model.common.UnknownPropertyPreserving;
import io.strimzi.crdgenerator.annotations.Description;
import io.sundr.builder.annotations.Buildable;
import lombok.EqualsAndHashCode;

import java.util.HashMap;
import java.util.Map;

/**
* Cruise Control's API users config
*/
@Buildable(
editableEnabled = false,
builderPackage = Constants.FABRIC8_KUBERNETES_API
)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"type", "valueFrom"})
@EqualsAndHashCode
public class ApiUsers implements UnknownPropertyPreserving {
public static final String TYPE_HASH_LOGIN_SERVICE = "hashLoginService";

private PasswordSource valueFrom;
private Map<String, Object> additionalProperties = new HashMap<>(0);

@Description("Must be `" + TYPE_HASH_LOGIN_SERVICE + "`")
@JsonInclude(JsonInclude.Include.NON_NULL)
public String getType() {
return TYPE_HASH_LOGIN_SERVICE;
}

@Description("Secret from which the custom Cruise Control API authentication credentials should be read. ")
@JsonProperty(required = true)
public PasswordSource getValueFrom() {
return valueFrom;
}

public void setValueFrom(PasswordSource valueFrom) {
this.valueFrom = valueFrom;
}

public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}

public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
"image", "tlsSidecar", "resources", "livenessProbe", "readinessProbe", "jvmOptions", "logging", "template",
"brokerCapacity", "config", "metricsConfig"})
"brokerCapacity", "config", "metricsConfig", "apiUsers"})
@EqualsAndHashCode
@ToString
public class CruiseControlSpec implements HasConfigurableMetrics, HasConfigurableLogging, HasLivenessProbe, HasReadinessProbe, UnknownPropertyPreserving {
Expand All @@ -60,6 +60,7 @@ public class CruiseControlSpec implements HasConfigurableMetrics, HasConfigurabl
private BrokerCapacity brokerCapacity;
private Map<String, Object> config = new HashMap<>(0);
private MetricsConfig metricsConfig;
private ApiUsers apiUsers;
private Map<String, Object> additionalProperties = new HashMap<>(0);

@Description("The container image used for Cruise Control pods. "
Expand Down Expand Up @@ -109,6 +110,16 @@ public void setConfig(Map<String, Object> config) {
this.config = config;
}

@Description("The Cruise Control `ApiUsers` configuration")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public ApiUsers getApiUsers() {
return this.apiUsers;
}

public void setApiUsers(ApiUsers apiUsers) {
this.apiUsers = apiUsers;
}

@Description("Metrics configuration.")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.strimzi.api.kafka.model.kafka.KafkaClusterSpec;
import io.strimzi.api.kafka.model.kafka.KafkaResources;
import io.strimzi.api.kafka.model.kafka.Storage;
import io.strimzi.api.kafka.model.kafka.cruisecontrol.ApiUsers;
import io.strimzi.api.kafka.model.kafka.cruisecontrol.CruiseControlResources;
import io.strimzi.api.kafka.model.kafka.cruisecontrol.CruiseControlSpec;
import io.strimzi.api.kafka.model.kafka.cruisecontrol.CruiseControlTemplate;
Expand All @@ -44,8 +45,9 @@
import io.strimzi.operator.common.Annotations;
import io.strimzi.operator.common.Reconciliation;
import io.strimzi.operator.common.Util;
import io.strimzi.operator.common.model.InvalidResourceException;
import io.strimzi.operator.common.model.Labels;
import io.strimzi.operator.common.model.PasswordGenerator;
import io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties;
import io.strimzi.operator.common.model.cruisecontrol.CruiseControlConfigurationParameters;

import java.io.IOException;
Expand All @@ -64,13 +66,6 @@
import static io.strimzi.operator.cluster.model.VolumeUtils.createVolumeMount;
import static io.strimzi.operator.cluster.model.cruisecontrol.CruiseControlConfiguration.CRUISE_CONTROL_DEFAULT_ANOMALY_DETECTION_GOALS;
import static io.strimzi.operator.cluster.model.cruisecontrol.CruiseControlConfiguration.CRUISE_CONTROL_GOALS;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_ADMIN_NAME;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_ADMIN_PASSWORD_KEY;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_ADMIN_ROLE;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_AUTH_FILE_KEY;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_USER_NAME;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_USER_PASSWORD_KEY;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_USER_ROLE;
import static java.lang.String.format;

/**
Expand All @@ -79,7 +74,6 @@
public class CruiseControl extends AbstractModel implements SupportsMetrics, SupportsLogging {
protected static final String COMPONENT_TYPE = "cruise-control";
protected static final String CRUISE_CONTROL_CONTAINER_NAME = "cruise-control";

protected static final String API_HEALTHCHECK_PATH = "/kafkacruisecontrol/state";
protected static final String TLS_CC_CERTS_VOLUME_NAME = "cc-certs";
protected static final String TLS_CC_CERTS_VOLUME_MOUNT = "/etc/cruise-control/cc-certs/";
Expand All @@ -100,7 +94,7 @@ public class CruiseControl extends AbstractModel implements SupportsMetrics, Sup
/**
* API auth credentials file name
*/
public static final String API_AUTH_CREDENTIALS_FILE = API_AUTH_CONFIG_VOLUME_MOUNT + API_AUTH_FILE_KEY;
public static final String API_AUTH_CREDENTIALS_FILE = API_AUTH_CONFIG_VOLUME_MOUNT + CruiseControlApiProperties.AUTH_FILE_KEY;

protected static final String ENV_VAR_CRUISE_CONTROL_METRICS_ENABLED = "CRUISE_CONTROL_METRICS_ENABLED";

Expand All @@ -121,6 +115,9 @@ public class CruiseControl extends AbstractModel implements SupportsMetrics, Sup

private boolean sslEnabled;
private boolean authEnabled;
private boolean apiUsersEnabled;
private String userManagedApiSecretName;
private String userManagedApiSecretKey;
@SuppressFBWarnings({"UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"}) // This field is initialized in the fromCrd method
protected Capacity capacity;
@SuppressFBWarnings({"UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR"}) // This field is initialized in the fromCrd method
Expand All @@ -141,7 +138,7 @@ public class CruiseControl extends AbstractModel implements SupportsMetrics, Sup

protected static final String ENV_VAR_API_SSL_ENABLED = "STRIMZI_CC_API_SSL_ENABLED";
protected static final String ENV_VAR_API_AUTH_ENABLED = "STRIMZI_CC_API_AUTH_ENABLED";
protected static final String ENV_VAR_API_USER = "API_USER";
protected static final String ENV_VAR_API_HEALTHCHECK_USERNAME = "API_HEALTHCHECK";
protected static final String ENV_VAR_API_PORT = "API_PORT";
protected static final String ENV_VAR_API_HEALTHCHECK_PATH = "API_HEALTHCHECK_PATH";

Expand Down Expand Up @@ -214,6 +211,13 @@ public static CruiseControl fromCrd(
result.sslEnabled = ccConfiguration.isApiSslEnabled();
result.authEnabled = ccConfiguration.isApiAuthEnabled();

result.apiUsersEnabled = isApiUsersConfigEnabled(ccSpec);
if (result.apiUsersEnabled()) {
ApiUsers apiUsers = ccSpec.getApiUsers();
result.userManagedApiSecretName = apiUsers.getValueFrom().getSecretKeyRef().getName();
result.userManagedApiSecretKey = apiUsers.getValueFrom().getSecretKeyRef().getKey();
}

// To avoid illegal storage configurations provided by the user,
// we rely on the storage configuration provided by the KafkaAssemblyOperator
result.capacity = new Capacity(reconciliation, kafkaCr.getSpec(), kafkaBrokerNodes, kafkaStorage, kafkaBrokerResources);
Expand Down Expand Up @@ -291,6 +295,47 @@ public void checkGoals(CruiseControlConfiguration configuration) {
}
}

/**
* @return True if API users are enabled. False otherwise.
*/
public boolean apiUsersEnabled() {
return apiUsersEnabled;
}

/**
* Checks if Cruise Control spec has valid ApiUsers config.
*
* @param ccSpec The Cruise Control spec to check.
*/
private static boolean isApiUsersConfigEnabled(CruiseControlSpec ccSpec) {
ApiUsers apiUsers = ccSpec.getApiUsers();
if (apiUsers != null) {
if (apiUsers.getValueFrom() == null
|| apiUsers.getValueFrom().getSecretKeyRef() == null
|| apiUsers.getValueFrom().getSecretKeyRef().getName() == null
|| apiUsers.getValueFrom().getSecretKeyRef().getKey() == null) {
throw new InvalidResourceException("Resource requests custom ApiUsers config but doesn't specify the secret name and key");
} else {
return true;
}
}
return false;
}

/**
* @return Returns user-managed API credentials secret name
*/
public String getUserManagedApiSecretName() {
return userManagedApiSecretName;
}

/**
* @return Returns user-managed API credentials secret key
*/
public String getUserManagedApiSecretKey() {
return userManagedApiSecretKey;
}

/**
* @return Generates a Kubernetes Service for Cruise Control
*/
Expand Down Expand Up @@ -394,7 +439,7 @@ protected List<EnvVar> getEnvVars() {

varList.add(ContainerUtils.createEnvVar(ENV_VAR_API_SSL_ENABLED, String.valueOf(this.sslEnabled)));
varList.add(ContainerUtils.createEnvVar(ENV_VAR_API_AUTH_ENABLED, String.valueOf(this.authEnabled)));
varList.add(ContainerUtils.createEnvVar(ENV_VAR_API_USER, API_USER_NAME));
varList.add(ContainerUtils.createEnvVar(ENV_VAR_API_HEALTHCHECK_USERNAME, CruiseControlApiProperties.HEALTHCHECK_USERNAME));
varList.add(ContainerUtils.createEnvVar(ENV_VAR_API_PORT, String.valueOf(REST_API_PORT)));
varList.add(ContainerUtils.createEnvVar(ENV_VAR_API_HEALTHCHECK_PATH, API_HEALTHCHECK_PATH));

Expand All @@ -410,58 +455,6 @@ protected List<EnvVar> getEnvVars() {
return varList;
}

/**
* Creates Cruise Control API auth usernames, passwords, and credentials file.
*
* @param passwordGenerator The password generator for API users.
* @param oldSecret The old secret.
* @param adminUser Additional admin user.
*
* @return Map containing Cruise Control API auth credentials.
*/
public static Map<String, String> generateCruiseControlApiCredentials(PasswordGenerator passwordGenerator,
Secret oldSecret,
CruiseControlUser adminUser) {
if (oldSecret != null) {
// The credentials should not change with every reconciliation
// So if the secret with credentials already exists, we re-use the values
// But we use the new secret to update labels etc. if needed
var data = oldSecret.getData();
var adminPassword = data.get(API_ADMIN_PASSWORD_KEY);
var userPassword = data.get(API_USER_PASSWORD_KEY);
var authFile = data.get(API_AUTH_FILE_KEY);
if (adminPassword == null || adminPassword.isBlank() || userPassword == null
|| userPassword.isBlank() || authFile == null || authFile.isBlank()) {
throw new RuntimeException(format("Secret %s is invalid", oldSecret.getMetadata().getName()));
} else if (!Util.decodeFromBase64(authFile).contains(API_ADMIN_NAME) || !Util.decodeFromBase64(authFile).contains(API_USER_NAME)) {
throw new RuntimeException(format("Secret %s has invalid authentication file", oldSecret.getMetadata().getName()));
} else {
return data;
}
} else {
String apiAdminPassword = passwordGenerator.generate();
String apiUserPassword = passwordGenerator.generate();

/*
* Create Cruise Control API auth credentials file following Jetty's
* HashLoginService's file format: username: password [,rolename ...]
*/
String authCredentialsFile =
API_ADMIN_NAME + ": " + apiAdminPassword + "," + API_ADMIN_ROLE + "\n" +
API_USER_NAME + ": " + apiUserPassword + "," + API_USER_ROLE + "\n";

if (adminUser != null) {
authCredentialsFile += adminUser.username() + ": " + adminUser.password() + "," + API_ADMIN_ROLE + "\n";
}

return Map.of(
API_ADMIN_PASSWORD_KEY, Util.encodeToBase64(apiAdminPassword),
API_USER_PASSWORD_KEY, Util.encodeToBase64(apiUserPassword),
API_AUTH_FILE_KEY, Util.encodeToBase64(authCredentialsFile)
);
}
}

/**
* Cruise Control user credentials.
*
Expand All @@ -482,15 +475,12 @@ public String toString() {
/**
* Generate the Secret containing the Cruise Control API auth credentials.
*
* @param passwordGenerator The password generator for API users.
* @param oldSecret The old secret.
* @param adminUser Additional admin user.
*
* @param data Map containing API credential data
* @return The generated Secret.
*/
public Secret generateApiSecret(PasswordGenerator passwordGenerator, Secret oldSecret, CruiseControlUser adminUser) {
public Secret generateApiSecret(Map<String, String> data) {
return ModelUtils.createSecret(CruiseControlResources.apiSecretName(cluster), namespace, labels, ownerReference,
generateCruiseControlApiCredentials(passwordGenerator, oldSecret, adminUser), Collections.emptyMap(), Collections.emptyMap());
data, Collections.emptyMap(), Collections.emptyMap());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_TO_ADMIN_NAME;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_TO_ADMIN_NAME_KEY;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.API_TO_ADMIN_PASSWORD_KEY;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.TOPIC_OPERATOR_PASSWORD_KEY;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.TOPIC_OPERATOR_USERNAME;
import static io.strimzi.operator.common.model.cruisecontrol.CruiseControlApiProperties.TOPIC_OPERATOR_USERNAME_KEY;
import static java.lang.String.format;

/**
Expand Down Expand Up @@ -302,8 +302,8 @@ private static Map<String, String> generateCruiseControlApiCredentials(Secret ol
// So if the secret with credentials already exists, we re-use the values
// But we use the new secret to update labels etc. if needed
var data = oldSecret.getData();
var username = data.get(API_TO_ADMIN_NAME_KEY);
var password = data.get(API_TO_ADMIN_PASSWORD_KEY);
var username = data.get(TOPIC_OPERATOR_USERNAME_KEY);
var password = data.get(TOPIC_OPERATOR_PASSWORD_KEY);
if (username == null || username.isBlank() || password == null || password.isBlank()) {
throw new RuntimeException(format("Secret %s is invalid", oldSecret.getMetadata().getName()));
} else {
Expand All @@ -313,8 +313,8 @@ private static Map<String, String> generateCruiseControlApiCredentials(Secret ol
PasswordGenerator passwordGenerator = new PasswordGenerator(16);
String apiToAdminPassword = passwordGenerator.generate();
return Map.of(
API_TO_ADMIN_NAME_KEY, Util.encodeToBase64(API_TO_ADMIN_NAME),
API_TO_ADMIN_PASSWORD_KEY, Util.encodeToBase64(apiToAdminPassword)
TOPIC_OPERATOR_USERNAME_KEY, Util.encodeToBase64(TOPIC_OPERATOR_USERNAME),
TOPIC_OPERATOR_PASSWORD_KEY, Util.encodeToBase64(apiToAdminPassword)
);
}
}
Expand Down
Loading

0 comments on commit ab23356

Please sign in to comment.