Skip to content
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
2 changes: 1 addition & 1 deletion docs/cluster-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The referenced secret must be of type `kubernetes.io/basic-auth` and contain the

Use this option when the credentials are mounted as a file instead of a Kubernetes Secret.

### File format
#### File format

The file must contain JSON with the following fields:

Expand Down
58 changes: 25 additions & 33 deletions operator/src/main/java/it/aboutbits/postgresql/core/FileRef.java
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
package it.aboutbits.postgresql.core;

import io.fabric8.generator.annotation.Required;
import io.fabric8.generator.annotation.ValidationRule;
import lombok.Getter;
import lombok.Setter;
import org.jspecify.annotations.NullMarked;

/// A reference to a file inside the operator container.
///
/// This class is used wherever a CRD spec needs to point to a specific file
/// The [#path] field identifies the file location within the container.
///
/// ### Example usage in a CR manifest
///
/// ```yaml
/// spec:
/// adminSecretFileRef:
/// path: "/mnt/secrets/db-credentials.json"
/// ```
@Getter
@Setter
@NullMarked
public class FileRef {
/// The path to the file.
/// Must not be blank.
@Required
@ValidationRule(
value = "self.trim().size() > 0",
message = "The path must not be empty."
)
private String path = "";
}
package it.aboutbits.postgresql.core;

import io.fabric8.generator.annotation.Required;
import io.fabric8.generator.annotation.ValidationRule;
import lombok.Getter;
import lombok.Setter;
import org.jspecify.annotations.NullMarked;

/// A reference to a file inside the operator container.
///
/// This class is used wherever a CRD spec needs to point to a specific file
/// The [#path] field identifies the file location within the container.
@Getter
@Setter
@NullMarked
public class FileRef {
/// The path to the file.
/// Must not be blank.
@Required
@ValidationRule(
value = "self.trim().size() > 0",
message = "The path must not be empty."
)
private String path = "";
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package it.aboutbits.postgresql.core;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.fabric8.kubernetes.client.KubernetesClient;
import it.aboutbits.postgresql.crd.clusterconnection.ClusterConnection;
Expand All @@ -19,18 +20,12 @@
@RequiredArgsConstructor
@NullMarked
public final class KubernetesService {
private final ObjectMapper objectMapper;

private record FileCredentials(
@Nullable String username,
@Nullable String password
) {
}

public static final String SECRET_TYPE_BASIC_AUTH = "kubernetes.io/basic-auth";
public static final String SECRET_DATA_BASIC_AUTH_USERNAME_KEY = "username";
public static final String SECRET_DATA_BASIC_AUTH_PASSWORD_KEY = "password";

private final ObjectMapper objectMapper;

public Credentials getAdminCredentials(
KubernetesClient kubernetesClient,
ClusterConnection clusterConnection
Expand All @@ -39,13 +34,15 @@ public Credentials getAdminCredentials(
if (spec.getAdminSecretRef() != null) {
var secretRef = spec.getAdminSecretRef();
var defaultNamespace = clusterConnection.getMetadata().getNamespace();

var credentials = getSecretRefCredentials(kubernetesClient, secretRef, defaultNamespace);
if (credentials.username() == null) {
var secretNamespace = getSecretNamespace(secretRef, defaultNamespace);
throw new IllegalStateException(
"The Secret reference is missing required data username [secret.namespace=%s, secret.name=%s]".formatted(
secretNamespace, secretRef.getName()));
}

return credentials;
} else if (spec.getAdminSecretFileRef() != null) {
return getSecretFileRefCredentials(spec.getAdminSecretFileRef());
Expand All @@ -60,20 +57,17 @@ public Credentials getSecretFileRefCredentials(FileRef fileRef) {
try (var in = Files.newInputStream(path)) {
var file = objectMapper.readValue(in, FileCredentials.class);
if (file.username() == null) {
throw new IllegalStateException(
"Credentials file is missing required field 'username' [path=%s]".formatted(path));
throw new IllegalStateException("Credentials file is missing required field 'username' [path=%s]".formatted(path));
}
if (file.password() == null) {
throw new IllegalStateException(
"Credentials file is missing required field 'password' [path=%s]".formatted(path));
throw new IllegalStateException("Credentials file is missing required field 'password' [path=%s]".formatted(path));
}

return new Credentials(file.username(), file.password());
} catch (NoSuchFileException e) {
throw new IllegalStateException(
"Credentials file not found [path=%s]".formatted(path), e);
throw new IllegalStateException("Credentials file not found [path=%s]".formatted(path), e);
} catch (IOException e) {
throw new IllegalStateException(
"Failed to read the credentials file [path=%s]".formatted(path), e);
throw new IllegalStateException("Failed to read the credentials file [path=%s]".formatted(path), e);
}
}

Expand Down Expand Up @@ -141,9 +135,21 @@ public Credentials getSecretRefCredentials(
);
}

private String getSecretNamespace(ResourceRef secretRef, String defaultNamespace) {
private String getSecretNamespace(
ResourceRef secretRef,
String defaultNamespace
) {
return secretRef.getNamespace() != null
? secretRef.getNamespace()
: defaultNamespace;
}

/// The JSON file may carry more keys than we need, for example, the AWS Secrets Manager
/// format also has `engine`, `host`, `port` and `dbname`. Unknown keys are ignored.
@JsonIgnoreProperties(ignoreUnknown = true)
private record FileCredentials(
@Nullable String username,
@Nullable String password
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
@SchemaCustomizer(value = HostCustomizer.class, input = "host")
@NullMarked
@ValidationRule(
value = "(has(self.adminSecretRef) ? 1 : 0) + (has(self.adminSecretFileRef) ? 1 : 0) == 1",
value = "has(self.adminSecretRef) != has(self.adminSecretFileRef)",
message = "Exactly one of 'adminSecretRef' or 'adminSecretFileRef' must be provided"
)
public class ClusterConnectionSpec {
Expand Down
Loading
Loading