Skip to content

Commit

Permalink
Nessie Kubernetes Operator: add support for GC
Browse files Browse the repository at this point in the history
Adds support for GC to the Nessie Kubernetes Operator:

1. New NessieGc CRD, supports both one-shot (Job) and
   scheduled (CronJob) GC setups.
2. Modified Nessie CRD with new gc spec option to
   automatically trigger scheduled GC operations for
   the Nessie deployment.
  • Loading branch information
adutra committed Aug 27, 2024
1 parent 934c2ce commit db7108f
Show file tree
Hide file tree
Showing 36 changed files with 1,728 additions and 12 deletions.
7 changes: 7 additions & 0 deletions operator/PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ resources:
group: nessie
kind: Nessie
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
domain: projectnessie.org
group: nessie
kind: NessieGc
version: v1alpha1
version: "3"
1 change: 1 addition & 0 deletions operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This project was bootstrapped using [Operator SDK]:
```bash
operator-sdk init --plugins=quarkus --domain=projectnessie.org --project-name=nessie-operator
operator-sdk create api --plugins=quarkus --group nessie --version=v1alpha1 --kind=Nessie
operator-sdk create api --plugins=quarkus --group nessie --version=v1alpha1 --kind=NessieGc
```

[Operator SDK]:https://sdk.operatorframework.io/docs/cli/operator-sdk/
41 changes: 32 additions & 9 deletions operator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ tasks.named("quarkusDependenciesBuild").configure { dependsOn("processJandexInde

tasks.named<Test>("intTest").configure {
dependsOn(buildNessieServerTestImage)
dependsOn(buildNessieGcTestImage)
// Required to install the CRDs during integration tests
val crdsDir = project.layout.buildDirectory.dir("kubernetes").get().asFile.toString()
systemProperty("nessie.crds.dir", crdsDir)
Expand All @@ -127,15 +128,6 @@ val buildNessieServerTestImage by
tasks.registering(Exec::class) {
dependsOn(":nessie-quarkus:quarkusBuild")
workingDir = project.layout.projectDirectory.asFile.parentFile
fun which(command: String): String? {
val stdout = ByteArrayOutputStream()
val result = exec {
isIgnoreExitValue = true
standardOutput = stdout
commandLine("which", command)
}
return if (result.exitValue == 0) "$stdout".trim() else null
}
executable =
which("docker")
?: which("podman")
Expand All @@ -149,3 +141,34 @@ val buildNessieServerTestImage by
"servers/quarkus-server"
)
}

// Builds the Nessie GC image to use in integration tests.
// The image will then be loaded into the running K3S cluster,
// see K3sContainerLifecycleManager.
val buildNessieGcTestImage by
tasks.registering(Exec::class) {
dependsOn(":nessie-gc-tool:shadowJar")
workingDir = project.layout.projectDirectory.asFile.parentFile
executable =
which("docker")
?: which("podman")
?: throw IllegalStateException("Neither docker nor podman found on the system")
args(
"build",
"--file",
"tools/dockerbuild/docker/Dockerfile-gctool",
"--tag",
"projectnessie/nessie-test-gc:" + project.version,
"gc/gc-tool"
)
}

private fun which(command: String): String? {
val stdout = ByteArrayOutputStream()
val result = exec {
isIgnoreExitValue = true
standardOutput = stdout
commandLine("which", command)
}
return if (result.exitValue == 0) "$stdout".trim() else null
}
12 changes: 12 additions & 0 deletions operator/examples/nessie-gc/nessie-gc-schedule.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: nessie.projectnessie.org/v1alpha1
kind: NessieGc
metadata:
name: nessie-scheduled-gc
namespace: nessie-ns
spec:
nessieRef:
name: nessie-simple
run: Periodically
schedule:
cron: "* * * * *"
timeZone: "UTC"
8 changes: 8 additions & 0 deletions operator/examples/nessie-gc/nessie-gc-simple.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: nessie.projectnessie.org/v1alpha1
kind: NessieGc
metadata:
name: nessie-simple-gc
namespace: nessie-ns
spec:
nessieRef:
name: nessie-simple
File renamed without changes.
11 changes: 11 additions & 0 deletions operator/examples/nessie/nessie-gc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: nessie.projectnessie.org/v1alpha1
kind: Nessie
metadata:
name: nessie-simple
namespace: nessie-ns
spec:
gc:
enabled: true
schedule:
cron: "* * * * *"
timeZone: UTC
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ spec:
log:
console.format: "%d{HH:mm:ss} %s%e%n"
category."org.projectnessie".level: "DEBUG"
gc:
enabled: true
schedule:
cron: "* * * * *"
timeZone: UTC
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ public class K3sContainerLifecycleManager extends AbstractContainerLifecycleMana
TOOL="$(which docker > /dev/null && echo docker || echo podman)"
${TOOL} image save projectnessie/nessie-test-server:$NESSIE_VERSION | \
${TOOL} exec --interactive $CONTAINER_NAME ctr images import --no-unpack -
${TOOL} image save projectnessie/nessie-test-gc:$NESSIE_VERSION | \
${TOOL} exec --interactive $CONTAINER_NAME ctr images import --no-unpack -
""";

@Target(ElementType.FIELD)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ public enum EventReason {
CreatingDeployment(EventType.Normal),
CreatingService(EventType.Normal),
CreatingMgmtService(EventType.Normal),
CreatingNessieGc(EventType.Normal),
CreatingServiceMonitor(EventType.Normal),
CreatingIngress(EventType.Normal),
CreatingHPA(EventType.Normal),
ReconcileSuccess(EventType.Normal),
GcCronJobActive(EventType.Normal),
GcJobComplete(EventType.Normal),

// Warning events
InvalidName(EventType.Warning),
Expand All @@ -42,7 +45,12 @@ public enum EventReason {
MultipleReplicasNotAllowed(EventType.Warning),
AutoscalingNotAllowed(EventType.Warning),
ServiceMonitorNotSupported(EventType.Warning),
InvalidGcConfig(EventType.Warning),
InMemoryGcNotRecommended(EventType.Warning),
NessieNotFound(EventType.Warning),
ReconcileError(EventType.Warning),
GcJobFailed(EventType.Warning),
GcCronJobSuspended(EventType.Warning),
;

private final EventType type;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.projectnessie.operator.exception.NessieOperatorException;
import org.projectnessie.operator.reconciler.nessie.NessieReconciler;
import org.projectnessie.operator.reconciler.nessie.resource.Nessie;
import org.projectnessie.operator.reconciler.nessiegc.NessieGcReconciler;
import org.projectnessie.operator.reconciler.nessiegc.resource.NessieGc;
import org.projectnessie.operator.utils.EventUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -126,7 +128,13 @@ private Event createOrUpdateEvent(
&& kce.getCode() == HttpURLConnection.HTTP_CONFLICT
&& attempt < 3) {
LOGGER.debug("Event was updated concurrently, retrying");
current = client.v1().events().resource(current).require();
current =
client
.v1()
.events()
.inNamespace(primary.getMetadata().getNamespace())
.withName(EventUtils.eventName(primary, reason))
.require();
return createOrUpdateEvent(attempt + 1, primary, reason, current, message, args);
}
LOGGER.warn("Failed to create or update event", e);
Expand Down Expand Up @@ -239,6 +247,7 @@ private Event editEvent(Event current, String formatted, String timestamp, Micro
private static String getComponent(HasMetadata primary) {
return switch (primary.getKind()) {
case Nessie.KIND -> NessieReconciler.NAME;
case NessieGc.KIND -> NessieGcReconciler.NAME;
default -> throw new IllegalArgumentException("Unknown kind " + primary.getKind());
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.projectnessie.operator.reconciler.nessie.NessieReconciler;
import org.projectnessie.operator.reconciler.nessie.resource.Nessie;
import org.projectnessie.operator.reconciler.nessiegc.NessieGcReconciler;
import org.projectnessie.operator.reconciler.nessiegc.resource.NessieGc;
import org.projectnessie.operator.utils.ResourceUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -117,6 +119,7 @@ public ObjectMetaBuilder metaBuilder(HasMetadata primary, String name) {
public static String managedBy(HasMetadata primary) {
return switch (primary.getKind()) {
case Nessie.KIND -> NessieReconciler.NAME;
case NessieGc.KIND -> NessieGcReconciler.NAME;
default ->
throw new IllegalArgumentException("Unsupported primary resource: " + primary.getKind());
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.projectnessie.operator.reconciler.nessie.dependent.IngressV1Dependent;
import org.projectnessie.operator.reconciler.nessie.dependent.MainServiceDependent;
import org.projectnessie.operator.reconciler.nessie.dependent.ManagementServiceDependent;
import org.projectnessie.operator.reconciler.nessie.dependent.NessieGcDependent;
import org.projectnessie.operator.reconciler.nessie.dependent.PersistentVolumeClaimDependent;
import org.projectnessie.operator.reconciler.nessie.dependent.ServiceAccountDependent;
import org.projectnessie.operator.reconciler.nessie.dependent.ServiceMonitorDependent;
Expand Down Expand Up @@ -108,6 +109,11 @@
type = ServiceMonitorDependent.class,
dependsOn = "service-mgmt",
activationCondition = ServiceMonitorDependent.ActivationCondition.class),
@Dependent(
name = "gc",
type = NessieGcDependent.class,
dependsOn = "service",
activationCondition = NessieGcDependent.ActivationCondition.class),
})
@RBACRule(apiGroups = "", resources = "events", verbs = RBACRule.ALL)
public class NessieReconciler extends AbstractReconciler<Nessie>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2024 Dremio
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.projectnessie.operator.reconciler.nessie.dependent;

import static org.projectnessie.operator.events.EventReason.CreatingNessieGc;
import static org.projectnessie.operator.reconciler.nessie.dependent.AbstractServiceAccountDependent.serviceAccountName;

import io.fabric8.kubernetes.api.model.LocalObjectReference;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import org.projectnessie.operator.events.EventService;
import org.projectnessie.operator.reconciler.KubernetesHelper;
import org.projectnessie.operator.reconciler.nessie.NessieReconciler;
import org.projectnessie.operator.reconciler.nessie.resource.Nessie;
import org.projectnessie.operator.reconciler.nessie.resource.options.GcOptions;
import org.projectnessie.operator.reconciler.nessie.resource.options.GcOptionsBuilder;
import org.projectnessie.operator.reconciler.nessiegc.resource.NessieGc;
import org.projectnessie.operator.reconciler.nessiegc.resource.NessieGcBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR)
public class NessieGcDependent extends CRUDKubernetesDependentResource<NessieGc, Nessie> {

private static final Logger LOGGER = LoggerFactory.getLogger(NessieGcDependent.class);

public NessieGcDependent() {
super(NessieGc.class);
}

@Override
public NessieGc create(NessieGc desired, Nessie nessie, Context<Nessie> context) {
LOGGER.debug(
"Creating nessiegc {} for {}",
desired.getMetadata().getName(),
nessie.getMetadata().getName());
EventService eventService = EventService.retrieveFromContext(context);
eventService.fireEvent(
nessie, CreatingNessieGc, "Creating nessiegc %s", desired.getMetadata().getName());
return super.create(desired, nessie, context);
}

@Override
public NessieGc desired(Nessie nessie, Context<Nessie> context) {
KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context);
GcOptions gc = nessie.getSpec().gc();
if (!gc.job().serviceAccount().create() && gc.job().serviceAccount().name() == null) {
// If no service account should be created, and no name has been provided, use the same
// service account used for the primary, instead of the default "default" service account
// that would be inferred otherwise by NessieGcReconciler.
gc =
new GcOptionsBuilder(gc)
.editJob()
.editServiceAccount()
.withName(serviceAccountName(nessie, nessie.getSpec().deployment().serviceAccount()))
.endServiceAccount()
.endJob()
.build();
}
return new NessieGcBuilder()
.withMetadata(helper.metaBuilder(nessie, nessie.getMetadata().getName() + "-gc").build())
.withNewSpec()
.withNessieRef(new LocalObjectReference(nessie.getMetadata().getName()))
.withSchedule(gc.schedule())
.withMark(gc.mark())
.withSweep(gc.sweep())
.withDatasource(gc.datasource())
.withIceberg(gc.iceberg())
.withJob(gc.job())
.endSpec()
.build();
}

public static class ActivationCondition implements Condition<NessieGc, Nessie> {

@Override
public boolean isMet(
DependentResource<NessieGc, Nessie> dependentResource,
Nessie nessie,
Context<Nessie> context) {
return nessie.getSpec().gc().enabled();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.projectnessie.operator.reconciler.nessie.resource.options.AuthenticationOptions;
import org.projectnessie.operator.reconciler.nessie.resource.options.AuthorizationOptions;
import org.projectnessie.operator.reconciler.nessie.resource.options.AutoscalingOptions;
import org.projectnessie.operator.reconciler.nessie.resource.options.GcOptions;
import org.projectnessie.operator.reconciler.nessie.resource.options.IngressOptions;
import org.projectnessie.operator.reconciler.nessie.resource.options.MonitoringOptions;
import org.projectnessie.operator.reconciler.nessie.resource.options.RemoteDebugOptions;
Expand Down Expand Up @@ -84,6 +85,7 @@ public record NessieSpec(
@JsonPropertyDescription("Nessie remote debugging options.") //
@Default("{}")
RemoteDebugOptions remoteDebug,
@JsonPropertyDescription("Nessie GC options.") @Default("{}") GcOptions gc,
@JsonPropertyDescription(
"""
Extra (advanced) configuration. \
Expand Down Expand Up @@ -134,7 +136,9 @@ public enum LogLevel {
}

public NessieSpec() {
this(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
this(
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null);
}

/**
Expand All @@ -158,6 +162,7 @@ public NessieSpec() {
monitoring = monitoring != null ? monitoring : new MonitoringOptions();
autoscaling = autoscaling != null ? autoscaling : new AutoscalingOptions();
remoteDebug = remoteDebug != null ? remoteDebug : new RemoteDebugOptions();
gc = gc != null ? gc : new GcOptions();
advancedConfig =
advancedConfig != null ? advancedConfig : JsonNodeFactory.instance.objectNode();
extraEnv = extraEnv != null ? List.copyOf(extraEnv) : List.of();
Expand Down
Loading

0 comments on commit db7108f

Please sign in to comment.