From 6d5185aa06e5e9e0d6fa1c1bfe05178663c8d283 Mon Sep 17 00:00:00 2001 From: Alexandre Dutra Date: Sun, 25 Feb 2024 17:19:32 +0100 Subject: [PATCH] Kubernetes Operator for Nessie --- .github/workflows/ci.yml | 73 ++- NOTICE | 29 + bom/build.gradle.kts | 1 + gradle/libs.versions.toml | 2 + gradle/projects.main.properties | 1 + operator/Makefile | 114 ++++ operator/PROJECT | 17 + operator/README.md | 16 + operator/build.gradle.kts | 149 ++++++ operator/examples/nessie-autoscaling.yaml | 24 + operator/examples/nessie-inmemory.yaml | 45 ++ operator/examples/nessie-postgres.yaml | 19 + operator/examples/nessie-rocks.yaml | 23 + operator/examples/nessie-simple.yaml | 6 + .../AbstractReconcilerIntegrationTests.java | 97 ++++ ...tractNessieReconcilerIntegrationTests.java | 174 ++++++ .../nessie/ITNessieReconcilerBigTable.java | 124 +++++ .../nessie/ITNessieReconcilerCassandra.java | 111 ++++ .../nessie/ITNessieReconcilerDynamo.java | 157 ++++++ .../nessie/ITNessieReconcilerJdbc.java | 131 +++++ .../nessie/ITNessieReconcilerMongo.java | 106 ++++ .../nessie/ITNessieReconcilerRocks.java | 116 ++++ .../AbstractContainerLifecycleManager.java | 89 +++ .../BigTableContainerLifecycleManager.java | 57 ++ .../CassandraContainerLifecycleManager.java | 63 +++ .../DynamoContainerLifecycleManager.java | 52 ++ .../K3sContainerLifecycleManager.java | 506 ++++++++++++++++++ .../KeycloakContainerLifecycleManager.java | 72 +++ .../MongoContainerLifecycleManager.java | 66 +++ .../PostgresContainerLifecycleManager.java | 51 ++ .../fixtures/bigtable/config-map.yaml | 23 + .../fixtures/bigtable/deployment.yaml | 96 ++++ .../inttests/fixtures/bigtable/nessie.yaml | 54 ++ .../inttests/fixtures/bigtable/secret.yaml | 12 + .../fixtures/bigtable/service-mgmt.yaml | 23 + .../fixtures/bigtable/service-monitor.yaml | 26 + .../inttests/fixtures/bigtable/service.yaml | 23 + .../fixtures/cassandra/config-map.yaml | 24 + .../fixtures/cassandra/deployment.yaml | 84 +++ .../inttests/fixtures/cassandra/init.cql | 17 + .../inttests/fixtures/cassandra/nessie.yaml | 57 ++ .../inttests/fixtures/cassandra/secret.yaml | 7 + .../fixtures/cassandra/service-account.yaml | 12 + .../fixtures/cassandra/service-mgmt.yaml | 23 + .../inttests/fixtures/cassandra/service.yaml | 23 + .../inttests/fixtures/dynamo/config-map.yaml | 20 + .../inttests/fixtures/dynamo/deployment.yaml | 94 ++++ .../inttests/fixtures/dynamo/ingress.yaml | 26 + .../inttests/fixtures/dynamo/nessie.yaml | 55 ++ .../inttests/fixtures/dynamo/secret.yaml | 8 + .../fixtures/dynamo/service-account.yaml | 12 + .../fixtures/dynamo/service-mgmt.yaml | 23 + .../inttests/fixtures/dynamo/service.yaml | 22 + .../inttests/fixtures/jdbc/autoscaler.yaml | 18 + .../inttests/fixtures/jdbc/config-map.yaml | 23 + .../inttests/fixtures/jdbc/deployment.yaml | 89 +++ .../inttests/fixtures/jdbc/nessie.yaml | 71 +++ .../inttests/fixtures/jdbc/secret.yaml | 7 + .../fixtures/jdbc/service-account.yaml | 14 + .../inttests/fixtures/jdbc/service-mgmt.yaml | 26 + .../inttests/fixtures/jdbc/service.yaml | 27 + .../inttests/fixtures/mongo/config-map.yaml | 21 + .../inttests/fixtures/mongo/deployment.yaml | 84 +++ .../inttests/fixtures/mongo/nessie.yaml | 49 ++ .../inttests/fixtures/mongo/secret.yaml | 7 + .../fixtures/mongo/service-account.yaml | 4 + .../inttests/fixtures/mongo/service-mgmt.yaml | 23 + .../inttests/fixtures/mongo/service.yaml | 23 + .../inttests/fixtures/rocks/config-map.yaml | 27 + .../inttests/fixtures/rocks/deployment.yaml | 89 +++ .../inttests/fixtures/rocks/nessie.yaml | 69 +++ .../operator/inttests/fixtures/rocks/pvc.yaml | 20 + .../fixtures/rocks/service-account.yaml | 14 + .../inttests/fixtures/rocks/service-mgmt.yaml | 23 + .../inttests/fixtures/rocks/service.yaml | 23 + .../testinfra/Dockerfile-bigtable-version | 5 + .../testinfra/Dockerfile-cassandra-version | 3 + .../testinfra/Dockerfile-dynamo-version | 3 + .../operator/testinfra/Dockerfile-k3s-version | 5 + .../testinfra/Dockerfile-mongo-version | 3 + .../testinfra/Dockerfile-postgres-version | 3 + .../operator/events/EventReason.java | 57 ++ .../operator/events/EventService.java | 245 +++++++++ .../operator/events/EventType.java | 21 + .../exception/InvalidSpecException.java | 32 ++ .../exception/NessieOperatorException.java | 23 + .../reconciler/AbstractReconciler.java | 112 ++++ .../operator/reconciler/KubernetesHelper.java | 170 ++++++ .../reconciler/nessie/NessieReconciler.java | 162 ++++++ ...tractHorizontalPodAutoscalerDependent.java | 71 +++ .../dependent/AbstractIngressDependent.java | 96 ++++ .../AbstractServiceAccountDependent.java | 94 ++++ .../nessie/dependent/ConfigMapDependent.java | 309 +++++++++++ .../nessie/dependent/DeploymentDependent.java | 442 +++++++++++++++ ...rizontalPodAutoscalerV2Beta1Dependent.java | 88 +++ ...rizontalPodAutoscalerV2Beta2Dependent.java | 93 ++++ .../HorizontalPodAutoscalerV2Dependent.java | 93 ++++ .../dependent/IngressV1Beta1Dependent.java | 134 +++++ .../nessie/dependent/IngressV1Dependent.java | 136 +++++ .../dependent/MainServiceDependent.java | 103 ++++ .../dependent/ManagementServiceDependent.java | 112 ++++ .../PersistentVolumeClaimDependent.java | 118 ++++ .../dependent/ServiceAccountDependent.java | 41 ++ .../dependent/ServiceMonitorDependent.java | 103 ++++ .../reconciler/nessie/resource/Nessie.java | 53 ++ .../nessie/resource/NessieSpec.java | 219 ++++++++ .../nessie/resource/NessieStatus.java | 81 +++ .../options/AuthenticationOptions.java | 78 +++ .../options/AuthorizationOptions.java | 62 +++ .../resource/options/AutoscalingOptions.java | 88 +++ .../resource/options/AwsCredentials.java | 44 ++ .../resource/options/BigTableOptions.java | 48 ++ .../resource/options/CassandraOptions.java | 51 ++ .../resource/options/DynamoDbOptions.java | 32 ++ .../nessie/resource/options/ImageOptions.java | 83 +++ .../resource/options/IngressOptions.java | 81 +++ .../nessie/resource/options/JdbcOptions.java | 69 +++ .../resource/options/MongoDbOptions.java | 41 ++ .../resource/options/MonitoringOptions.java | 54 ++ .../nessie/resource/options/ProbeOptions.java | 77 +++ .../resource/options/RemoteDebugOptions.java | 48 ++ .../resource/options/RocksDbOptions.java | 50 ++ .../nessie/resource/options/SecretValue.java | 33 ++ .../options/ServiceAccountOptions.java | 53 ++ .../resource/options/ServiceOptions.java | 80 +++ .../resource/options/TelemetryOptions.java | 75 +++ .../options/VersionStoreCacheOptions.java | 103 ++++ .../resource/options/VersionStoreOptions.java | 132 +++++ .../resource/options/WorkloadOptions.java | 118 ++++ .../operator/utils/EventUtils.java | 119 ++++ .../operator/utils/ResourceUtils.java | 56 ++ operator/src/main/kubernetes/nessie.svg | 144 +++++ .../src/main/resources/application.properties | 61 +++ operator/src/main/resources/nessie-banner.txt | 8 + .../AbstractReconcilerUnitTests.java | 176 ++++++ .../nessie/TestNessieReconcilerBigTable.java | 92 ++++ .../nessie/TestNessieReconcilerCassandra.java | 92 ++++ .../nessie/TestNessieReconcilerInMemory.java | 130 +++++ .../nessie/TestNessieReconcilerJdbc.java | 96 ++++ .../nessie/TestNessieReconcilerMongo.java | 96 ++++ .../nessie/TestNessieReconcilerRocks.java | 97 ++++ .../operator/utils/TestEventUtils.java | 87 +++ .../operator/utils/TestResourceUtils.java | 62 +++ .../tests/fixtures/bigtable/autoscaler.yaml | 37 ++ .../tests/fixtures/bigtable/config-map.yaml | 37 ++ .../tests/fixtures/bigtable/deployment.yaml | 126 +++++ .../tests/fixtures/bigtable/ingress.yaml | 34 ++ .../tests/fixtures/bigtable/nessie.yaml | 119 ++++ .../tests/fixtures/bigtable/service-mgmt.yaml | 26 + .../fixtures/bigtable/service-monitor.yaml | 26 + .../tests/fixtures/bigtable/service.yaml | 26 + .../tests/fixtures/cassandra/autoscaler.yaml | 37 ++ .../tests/fixtures/cassandra/config-map.yaml | 37 ++ .../tests/fixtures/cassandra/deployment.yaml | 126 +++++ .../tests/fixtures/cassandra/ingress.yaml | 34 ++ .../tests/fixtures/cassandra/nessie.yaml | 118 ++++ .../fixtures/cassandra/service-mgmt.yaml | 23 + .../fixtures/cassandra/service-monitor.yaml | 26 + .../tests/fixtures/cassandra/service.yaml | 23 + .../tests/fixtures/inmemory/config-map.yaml | 16 + .../tests/fixtures/inmemory/deployment.yaml | 101 ++++ .../tests/fixtures/inmemory/nessie.yaml | 20 + .../tests/fixtures/inmemory/service-mgmt.yaml | 23 + .../tests/fixtures/inmemory/service.yaml | 23 + .../tests/fixtures/jdbc/autoscaler.yaml | 41 ++ .../tests/fixtures/jdbc/config-map.yaml | 35 ++ .../tests/fixtures/jdbc/deployment.yaml | 126 +++++ .../operator/tests/fixtures/jdbc/ingress.yaml | 34 ++ .../operator/tests/fixtures/jdbc/nessie.yaml | 112 ++++ .../tests/fixtures/jdbc/service-account.yaml | 14 + .../tests/fixtures/jdbc/service-mgmt.yaml | 23 + .../tests/fixtures/jdbc/service-monitor.yaml | 26 + .../operator/tests/fixtures/jdbc/service.yaml | 23 + .../tests/fixtures/mongo/autoscaler.yaml | 41 ++ .../tests/fixtures/mongo/config-map.yaml | 35 ++ .../tests/fixtures/mongo/deployment.yaml | 126 +++++ .../tests/fixtures/mongo/ingress.yaml | 34 ++ .../operator/tests/fixtures/mongo/nessie.yaml | 112 ++++ .../tests/fixtures/mongo/service-account.yaml | 14 + .../tests/fixtures/mongo/service-mgmt.yaml | 23 + .../tests/fixtures/mongo/service-monitor.yaml | 26 + .../tests/fixtures/mongo/service.yaml | 23 + .../tests/fixtures/rocks/config-map.yaml | 36 ++ .../tests/fixtures/rocks/deployment.yaml | 126 +++++ .../tests/fixtures/rocks/ingress.yaml | 32 ++ .../operator/tests/fixtures/rocks/nessie.yaml | 109 ++++ .../operator/tests/fixtures/rocks/pvc.yaml | 23 + .../tests/fixtures/rocks/service-account.yaml | 14 + .../tests/fixtures/rocks/service-mgmt.yaml | 23 + .../tests/fixtures/rocks/service-monitor.yaml | 26 + .../tests/fixtures/rocks/service.yaml | 23 + .../reconciler/AbstractReconcilerTests.java | 321 +++++++++++ .../build.gradle.kts | 1 + 193 files changed, 12688 insertions(+), 4 deletions(-) create mode 100644 operator/Makefile create mode 100644 operator/PROJECT create mode 100644 operator/README.md create mode 100644 operator/build.gradle.kts create mode 100644 operator/examples/nessie-autoscaling.yaml create mode 100644 operator/examples/nessie-inmemory.yaml create mode 100644 operator/examples/nessie-postgres.yaml create mode 100644 operator/examples/nessie-rocks.yaml create mode 100644 operator/examples/nessie-simple.yaml create mode 100644 operator/src/intTest/java/org/projectnessie/operator/reconciler/AbstractReconcilerIntegrationTests.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/AbstractNessieReconcilerIntegrationTests.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerBigTable.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerCassandra.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerDynamo.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerJdbc.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerMongo.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerRocks.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/testinfra/AbstractContainerLifecycleManager.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/testinfra/BigTableContainerLifecycleManager.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/testinfra/CassandraContainerLifecycleManager.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/testinfra/DynamoContainerLifecycleManager.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/testinfra/K3sContainerLifecycleManager.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/testinfra/KeycloakContainerLifecycleManager.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/testinfra/MongoContainerLifecycleManager.java create mode 100644 operator/src/intTest/java/org/projectnessie/operator/testinfra/PostgresContainerLifecycleManager.java create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/config-map.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/deployment.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/nessie.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/secret.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service-mgmt.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service-monitor.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/config-map.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/deployment.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/init.cql create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/nessie.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/secret.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service-account.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service-mgmt.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/config-map.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/deployment.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/ingress.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/nessie.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/secret.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service-account.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service-mgmt.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/autoscaler.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/config-map.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/deployment.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/nessie.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/secret.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service-account.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service-mgmt.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/config-map.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/deployment.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/nessie.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/secret.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service-account.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service-mgmt.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/config-map.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/deployment.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/nessie.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/pvc.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service-account.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service-mgmt.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service.yaml create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-bigtable-version create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-cassandra-version create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-dynamo-version create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-k3s-version create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-mongo-version create mode 100644 operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-postgres-version create mode 100644 operator/src/main/java/org/projectnessie/operator/events/EventReason.java create mode 100644 operator/src/main/java/org/projectnessie/operator/events/EventService.java create mode 100644 operator/src/main/java/org/projectnessie/operator/events/EventType.java create mode 100644 operator/src/main/java/org/projectnessie/operator/exception/InvalidSpecException.java create mode 100644 operator/src/main/java/org/projectnessie/operator/exception/NessieOperatorException.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/AbstractReconciler.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/KubernetesHelper.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/NessieReconciler.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractHorizontalPodAutoscalerDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractIngressDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractServiceAccountDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ConfigMapDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/DeploymentDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Beta1Dependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Beta2Dependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Dependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/IngressV1Beta1Dependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/IngressV1Dependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/MainServiceDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ManagementServiceDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/PersistentVolumeClaimDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ServiceAccountDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ServiceMonitorDependent.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/Nessie.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/NessieSpec.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/NessieStatus.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AuthenticationOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AuthorizationOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AutoscalingOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AwsCredentials.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/BigTableOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/CassandraOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/DynamoDbOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ImageOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/IngressOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/JdbcOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/MongoDbOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/MonitoringOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ProbeOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/RemoteDebugOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/RocksDbOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/SecretValue.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ServiceAccountOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ServiceOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/TelemetryOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/VersionStoreCacheOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/VersionStoreOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/WorkloadOptions.java create mode 100644 operator/src/main/java/org/projectnessie/operator/utils/EventUtils.java create mode 100644 operator/src/main/java/org/projectnessie/operator/utils/ResourceUtils.java create mode 100644 operator/src/main/kubernetes/nessie.svg create mode 100644 operator/src/main/resources/application.properties create mode 100644 operator/src/main/resources/nessie-banner.txt create mode 100644 operator/src/test/java/org/projectnessie/operator/reconciler/AbstractReconcilerUnitTests.java create mode 100644 operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerBigTable.java create mode 100644 operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerCassandra.java create mode 100644 operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerInMemory.java create mode 100644 operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerJdbc.java create mode 100644 operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerMongo.java create mode 100644 operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerRocks.java create mode 100644 operator/src/test/java/org/projectnessie/operator/utils/TestEventUtils.java create mode 100644 operator/src/test/java/org/projectnessie/operator/utils/TestResourceUtils.java create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/autoscaler.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/config-map.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/deployment.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/ingress.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/nessie.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service-mgmt.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service-monitor.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/autoscaler.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/config-map.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/deployment.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/ingress.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/nessie.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service-mgmt.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service-monitor.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/config-map.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/deployment.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/nessie.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/service-mgmt.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/service.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/autoscaler.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/config-map.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/deployment.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/ingress.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/nessie.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-account.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-mgmt.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-monitor.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/autoscaler.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/config-map.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/deployment.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/ingress.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/nessie.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-account.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-mgmt.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-monitor.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/config-map.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/deployment.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/ingress.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/nessie.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/pvc.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-account.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-mgmt.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-monitor.yaml create mode 100644 operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service.yaml create mode 100644 operator/src/testFixtures/java/org/projectnessie/operator/reconciler/AbstractReconcilerTests.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 873abbe0307..f2be69efe62 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,14 +88,15 @@ jobs: -x :nessie-quarkus:compileAll \ -x :nessie-server-admin-tool:compileAll \ -x :nessie-events-quarkus:compileAll \ + -x :nessie-operator:compileAll \ --scan - name: Gradle / Compile Quarkus run: | # 2 Retries - to mitigate https://github.com/gradle/gradle/issues/25751 - ./gradlew :nessie-quarkus:compileAll :nessie-server-admin-tool:compileAll :nessie-events-quarkus:compileAll --scan || \ - ./gradlew :nessie-quarkus:compileAll :nessie-server-admin-tool:compileAll :nessie-events-quarkus:compileAll --scan || \ - ./gradlew :nessie-quarkus:compileAll :nessie-server-admin-tool:compileAll :nessie-events-quarkus:compileAll --scan + ./gradlew :nessie-quarkus:compileAll :nessie-server-admin-tool:compileAll :nessie-events-quarkus:compileAll :nessie-operator:compileAll --scan || \ + ./gradlew :nessie-quarkus:compileAll :nessie-server-admin-tool:compileAll :nessie-events-quarkus:compileAll :nessie-operator:compileAll --scan || \ + ./gradlew :nessie-quarkus:compileAll :nessie-server-admin-tool:compileAll :nessie-events-quarkus:compileAll :nessie-operator:compileAll --scan - name: Gradle / Code checks run: ./gradlew codeChecks --scan @@ -141,7 +142,7 @@ jobs: java-version: ${{ matrix.java-version }} - name: Gradle / test - run: ./gradlew test :nessie-client:check -x :nessie-client:intTest -x :nessie-quarkus:test -x :nessie-server-admin-tool:test -x :nessie-events-quarkus:test --scan + run: ./gradlew test :nessie-client:check -x :nessie-client:intTest -x :nessie-quarkus:test -x :nessie-server-admin-tool:test -x :nessie-events-quarkus:test -x :nessie-operator:test --scan - name: Capture Test Reports uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 @@ -196,6 +197,13 @@ jobs: ./gradlew :nessie-events-quarkus:test --scan || \ ./gradlew :nessie-events-quarkus:test --scan + - name: Gradle / Test Quarkus Operator + run: | + # 2 Retries - to mitigate https://github.com/gradle/gradle/issues/25751 + ./gradlew :nessie-operator:test --scan || \ + ./gradlew :nessie-operator:test --scan || \ + ./gradlew :nessie-operator:test --scan + - name: Dump quarkus.log if: ${{ failure() }} run: | @@ -263,6 +271,7 @@ jobs: -x :nessie-quarkus:intTest \ -x :nessie-server-admin-tool:intTest \ -x :nessie-events-quarkus:intTest \ + -x :nessie-operator:intTest \ $(cat ../persist-prjs.txt) \ $(cat ../storage-prjs.txt) \ $(cat ../spark-prjs.txt) \ @@ -544,6 +553,61 @@ jobs: job-name: 'int-test-quarkus-events' java-version: ${{ matrix.java-version }} + int-test-quarkus-operator: + name: CI intTest Quarkus Operator + runs-on: ubuntu-22.04 + strategy: + max-parallel: 1 + matrix: + java-version: ['17'] + steps: + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - name: Setup runner + uses: ./.github/actions/setup-runner + - name: Setup Java, Gradle + uses: ./.github/actions/dev-tool-java + with: + java-version: ${{ matrix.java-version }} + + - name: Prepare Gradle build cache + uses: ./.github/actions/ci-incr-build-cache-prepare + with: + java-version: ${{ matrix.java-version }} + + - name: Gradle / intTest Operator + uses: gradle/actions/setup-gradle@v3 + with: + arguments: | + :nessie-operator:intTest + --scan + + - name: Dump quarkus.log + if: ${{ failure() }} + run: | + find . -path "**/build/quarkus.log" | while read ql ; do + echo "::group::Quarkus build log $ql" + cat $ql + echo "::endgroup::" + done + + - name: Capture Test Reports + uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: ci-inttest-operator-reports + path: | + **/build/quarkus.log + **/build/reports/* + **/build/test-results/* + retention-days: 7 + + - name: Save partial Gradle build cache + uses: ./.github/actions/ci-incr-build-cache-save + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + with: + job-name: 'int-test-quarkus-operator' + java-version: ${{ matrix.java-version }} + determine-jobs: name: CI Determine jobs runs-on: ubuntu-22.04 @@ -1067,6 +1131,7 @@ jobs: - int-test-quarkus-server - int-test-quarkus-tool - int-test-quarkus-events + - int-test-quarkus-operator strategy: max-parallel: 2 matrix: diff --git a/NOTICE b/NOTICE index 6924871ccb9..35673f0ae34 100755 --- a/NOTICE +++ b/NOTICE @@ -220,6 +220,35 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +--- +dk.brics.automaton:automaton + +Copyright (c) 2001-2022 Anders Moeller +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + --- io.github.crac:org-crac diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts index bb11e55f3e1..6271e67a82a 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { api(project(":nessie-keycloak-testcontainer")) api(project(":nessie-minio-testcontainer")) api(project(":nessie-nessie-testcontainer")) + api(project(":nessie-operator")) api(project(":nessie-quarkus-auth")) api(project(":nessie-quarkus-common")) api(project(":nessie-server-admin-tool")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6a6abb3315..391ddcd55b8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -132,6 +132,7 @@ quarkus-bom = { module = "io.quarkus.platform:quarkus-bom", version.ref = "quark quarkus-cassandra-bom = { module = "io.quarkus.platform:quarkus-cassandra-bom", version.ref = "quarkusPlatform" } quarkus-google-cloud-services-bom = { module = "io.quarkus.platform:quarkus-google-cloud-services-bom", version.ref = "quarkusPlatform" } quarkus-logging-sentry = { module = "io.quarkiverse.loggingsentry:quarkus-logging-sentry", version = "2.0.7" } +quarkus-operator-sdk-bom = { module = "io.quarkus.platform:quarkus-operator-sdk-bom", version.ref = "quarkusPlatform" } rest-assured = { module = "io.rest-assured:rest-assured", version = "5.4.0" } rocksdb-jni = { module = "org.rocksdb:rocksdbjni", version = "9.2.1" } scala-library-v212 = { module = "org.scala-lang:scala-library", version = { strictly = "[2.12, 2.13[", prefer = "2.12.19" }} @@ -148,6 +149,7 @@ spark-sql-v34-v212 = { module = "org.apache.spark:spark-sql_2_12", version = { s spark-sql-v34-v213 = { module = "org.apache.spark:spark-sql_2_13", version = { strictly = "[3.4, 3.5[", prefer = "3.4.2"}} spark-sql-v35-v212 = { module = "org.apache.spark:spark-sql_2_12", version = { strictly = "[3.5, 3.6[", prefer = "3.5.1"}} spark-sql-v35-v213 = { module = "org.apache.spark:spark-sql_2_13", version = { strictly = "[3.5, 3.6[", prefer = "3.5.1"}} +sundr-builder-annotations = { module = "io.sundr:builder-annotations", version = "0.103.1" } testcontainers-bom = { module = "org.testcontainers:testcontainers-bom", version = "1.19.8" } testcontainers-keycloak = { module = "com.github.dasniko:testcontainers-keycloak", version = "3.4.0" } threeten-extra = { module = "org.threeten:threeten-extra", version = "1.8.0" } diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties index 1d1c347cf26..ba589210e0c 100644 --- a/gradle/projects.main.properties +++ b/gradle/projects.main.properties @@ -32,6 +32,7 @@ nessie-jaxrs-tests=servers/jax-rs-tests nessie-keycloak-testcontainer=testing/keycloak-container nessie-nessie-testcontainer=testing/nessie-container nessie-object-storage-mock=testing/object-storage-mock +nessie-operator=operator nessie-quarkus-auth=servers/quarkus-auth nessie-quarkus-common=servers/quarkus-common nessie-quarkus=servers/quarkus-server diff --git a/operator/Makefile b/operator/Makefile new file mode 100644 index 00000000000..e0a5ba5a72b --- /dev/null +++ b/operator/Makefile @@ -0,0 +1,114 @@ + +VERSION ?= $(shell cat ../version.txt | sed -e 's/.*-SNAPSHOT/latest/g') +RELEASE_VERSION ?= $(shell cat ../version.txt | sed -e 's/-SNAPSHOT//g') + +# CHANNELS define the bundle channels used in the bundle. +# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable") +# To re-generate a bundle for other specific channels without changing the standard setup, you can: +# - use the CHANNELS as arg of the bundle target (e.g make bundle CHANNELS=candidate,fast,stable) +# - use environment variables to overwrite this value (e.g export CHANNELS="candidate,fast,stable") +ifneq ($(origin CHANNELS), undefined) +BUNDLE_CHANNELS := --channels=$(CHANNELS) +endif + +# DEFAULT_CHANNEL defines the default channel used in the bundle. +# Add a new line here if you would like to change its default config. (E.g DEFAULT_CHANNEL = "stable") +# To re-generate a bundle for any other default channel without changing the default setup, you can: +# - use the DEFAULT_CHANNEL as arg of the bundle target (e.g make bundle DEFAULT_CHANNEL=stable) +# - use environment variables to overwrite this value (e.g export DEFAULT_CHANNEL="stable") +ifneq ($(origin DEFAULT_CHANNEL), undefined) +BUNDLE_DEFAULT_CHANNEL := --default-channel=$(DEFAULT_CHANNEL) +endif +BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL) + +# IMAGE_TAG_BASE defines the docker.io namespace and part of the image name for remote images. +# This variable is used to construct full image tags for bundle and catalog images. +IMAGE_TAG_BASE ?= ghcr.io/projectnessie/nessie-operator + +# BUNDLE_IMG defines the image:tag used for the bundle. +# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=/:) +BUNDLE_IMG ?= $(IMAGE_TAG_BASE)-bundle:$(VERSION) + +# Image URL to use all building/pushing image targets +IMG ?= $(IMAGE_TAG_BASE):$(VERSION) + +PULL_POLICY ?= $(shell [ "$(VERSION)" = "latest" ] && echo "Always" || echo "IfNotPresent") +PLATFORM ?= linux/$(shell arch) + +all: docker-build + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Build + +docker-build: ## Build docker image with the manager. + ../gradlew :nessie-operator:spotlessApply :nessie-operator:clean :nessie-operator:build -x check \ + -Dquarkus.container-image.build=true \ + -Dquarkus.container-image.image=${IMG} \ + -Dquarkus.jib.platforms=${PLATFORM} \ + -Dquarkus.kubernetes.prometheus.generate-service-monitor=false \ + -Dquarkus.kubernetes.image-pull-policy=${PULL_POLICY} + +docker-push: ## Build and push docker image with the manager. + ../gradlew :nessie-operator:spotlessApply :nessie-operator:clean :nessie-operator:build -x check \ + -Dquarkus.container-image.build=true \ + -Dquarkus.container-image.push=true \ + -Dquarkus.container-image.image=${IMG} \ + -Dquarkus.jib.platforms=${PLATFORM} \ + -Dquarkus.kubernetes.prometheus.generate-service-monitor=false \ + -Dquarkus.kubernetes.image-pull-policy=${PULL_POLICY} + +##@ Deployment + +install: ## Install CRDs into the K8s cluster specified in ~/.kube/config. + @$(foreach file, $(wildcard build/kubernetes/*-v1.yml), kubectl apply -f $(file);) + +uninstall: ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. + @$(foreach file, $(wildcard build/kubernetes/*-v1.yml), kubectl delete -f $(file);) + +deploy: ## Deploy controller to the K8s cluster specified in ~/.kube/config. + kubectl apply -f build/kubernetes/kubernetes.yml + +undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. + kubectl delete -f build/kubernetes/kubernetes.yml + +##@ Helm + +helm-install: ## Install CRDs and the operator using Helm. + helm install nessie-operator build/helm -n nessie-operator + +helm-upgrade: ## Upgrade CRDs and the operator using Helm. + helm upgrade nessie-operator build/helm -n nessie-operator + +helm-uninstall: ## Uninstall CRDs and the operator using Helm. + helm uninstall nessie-operator -n nessie-operator + +##@ Bundle + +.PHONY: bundle +bundle: ## Generate bundle manifests and metadata, then validate generated files. + cat build/kubernetes/* | operator-sdk generate bundle -q --overwrite --version $(RELEASE_VERSION) $(BUNDLE_METADATA_OPTS) + operator-sdk bundle validate ./bundle + # TODO use quarkus + +.PHONY: bundle-build +bundle-build: ## Build the bundle image. + docker build -f build/bundle/nessie-operator/bundle.Dockerfile -t $(BUNDLE_IMG) build/bundle/nessie-operator + +.PHONY: bundle-push +bundle-push: ## Push the bundle image. + docker push $(BUNDLE_IMG) diff --git a/operator/PROJECT b/operator/PROJECT new file mode 100644 index 00000000000..b80524b72fb --- /dev/null +++ b/operator/PROJECT @@ -0,0 +1,17 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: projectnessie.org +layout: +- quarkus.javaoperatorsdk.io/v1-alpha +projectName: nessie-operator +resources: +- api: + crdVersion: v1 + namespaced: true + domain: projectnessie.org + group: nessie + kind: Nessie + version: v1alpha1 +version: "3" diff --git a/operator/README.md b/operator/README.md new file mode 100644 index 00000000000..1cc559a073b --- /dev/null +++ b/operator/README.md @@ -0,0 +1,16 @@ +# Kubernetes Operator for Nessie + +This module is a Kubernetes Operator for Nessie. + +**WARNING: This is a work in progress and is not ready for production use.** + +The operator is designed to manage the lifecycle of Nessie instances in a Kubernetes cluster. + +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]:https://sdk.operatorframework.io/docs/cli/operator-sdk/ diff --git a/operator/build.gradle.kts b/operator/build.gradle.kts new file mode 100644 index 00000000000..959722a5775 --- /dev/null +++ b/operator/build.gradle.kts @@ -0,0 +1,149 @@ +/* + * 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. + */ + +/* + * 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. + */ + +import java.io.ByteArrayOutputStream +import org.apache.tools.ant.filters.ReplaceTokens + +plugins { + alias(libs.plugins.quarkus) + id("nessie-conventions-quarkus") + id("nessie-jacoco") + id("nessie-license-report") +} + +extra["maven.name"] = "Nessie - Kubernetes Operator" + +dependencies { + implementation(enforcedPlatform(libs.quarkus.bom)) + implementation(platform(libs.quarkus.operator.sdk.bom)) + implementation("io.quarkiverse.operatorsdk:quarkus-operator-sdk") + implementation("io.quarkiverse.operatorsdk:quarkus-operator-sdk-bundle-generator") + implementation("io.quarkus:quarkus-micrometer-registry-prometheus") + implementation("io.quarkus:quarkus-container-image-jib") + + implementation("org.bouncycastle:bcpkix-jdk18on") + + compileOnly(libs.sundr.builder.annotations) + compileOnly("io.fabric8:generator-annotations") + + annotationProcessor(enforcedPlatform(libs.quarkus.bom)) + annotationProcessor(libs.sundr.builder.annotations) + // see https://github.com/sundrio/sundrio/issues/104 + annotationProcessor("io.fabric8:kubernetes-client") + + testFixturesApi(enforcedPlatform(libs.quarkus.bom)) + testFixturesApi(platform(libs.quarkus.operator.sdk.bom)) + testFixturesApi(platform(libs.junit.bom)) + testFixturesApi("io.quarkus:quarkus-junit5") + testFixturesApi("io.fabric8:openshift-client") + testFixturesApi(libs.bundles.junit.testing) + testFixturesApi(libs.awaitility) + + testImplementation("io.quarkus:quarkus-test-kubernetes-client") + testImplementation("io.fabric8:kubernetes-server-mock") + + intTestImplementation(platform(libs.testcontainers.bom)) + intTestImplementation(project(":nessie-client")) + intTestImplementation("org.testcontainers:k3s") + intTestImplementation("org.testcontainers:mongodb") + intTestImplementation("org.testcontainers:postgresql") + intTestImplementation("org.testcontainers:cassandra") + intTestImplementation(platform(libs.cassandra.driver.bom)) + intTestImplementation("com.datastax.oss:java-driver-core") + intTestImplementation(project(":nessie-keycloak-testcontainer")) + intTestImplementation(project(":nessie-container-spec-helper")) + + intTestCompileOnly(libs.microprofile.openapi) +} + +listOf("javadoc", "sourcesJar").forEach { name -> + tasks.named(name).configure { dependsOn(tasks.named("compileQuarkusGeneratedSourcesJava")) } +} + +listOf("checkstyleTest", "compileTestJava").forEach { name -> + tasks.named(name).configure { dependsOn(tasks.named("compileQuarkusTestGeneratedSourcesJava")) } +} + +tasks.named("processTestResources").configure { + inputs.property("projectVersion", project.version) + filter(ReplaceTokens::class, mapOf("tokens" to mapOf("projectVersion" to project.version))) +} + +tasks.named("processIntTestResources").configure { + inputs.property("projectVersion", project.version) + filter(ReplaceTokens::class, mapOf("tokens" to mapOf("projectVersion" to project.version))) +} + +tasks.named("quarkusAppPartsBuild").configure { + outputs.dir(project.layout.buildDirectory.dir("helm")) + outputs.dir(project.layout.buildDirectory.dir("kubernetes")) + outputs.dir(project.layout.buildDirectory.dir("bundle")) +} + +tasks.named("intTest").configure { + dependsOn(buildNessieServerTestImage) + // Required to install the CRDs during integration tests + val crdsDir = project.layout.buildDirectory.dir("kubernetes").get().asFile.toString() + systemProperty("nessie.crds.dir", crdsDir) + // Required for Ingress tests + systemProperty("jdk.httpclient.allowRestrictedHeaders", "host") +} + +// Builds the Nessie server image to use in integration tests. +// The image will then be loaded into the running K3S cluster, +// see K3sContainerLifecycleManager. +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") + ?: throw IllegalStateException("Neither docker nor podman found on the system") + args( + "build", + "--file", + "tools/dockerbuild/docker/Dockerfile-server", + "--tag", + "projectnessie/nessie-test-server:" + project.version, + "servers/quarkus-server" + ) + } diff --git a/operator/examples/nessie-autoscaling.yaml b/operator/examples/nessie-autoscaling.yaml new file mode 100644 index 00000000000..7afe8c4d489 --- /dev/null +++ b/operator/examples/nessie-autoscaling.yaml @@ -0,0 +1,24 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-autoscaling +spec: + size: 1 + logLevel: INFO + deployment: + image: + repository: ghcr.io/projectnessie/nessie + versionStore: + type: Jdbc + jdbc: + # helm install nessie-postgres oci://registry-1.docker.io/bitnamicharts/postgresql + url: jdbc:postgresql://nessie-postgres-postgresql:5432/nessiedb?currentSchema=nessie + username: postgres + password: + secret: postgres-creds + key: postgres_password + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 3 + targetCpuUtilizationPercentage: 50 diff --git a/operator/examples/nessie-inmemory.yaml b/operator/examples/nessie-inmemory.yaml new file mode 100644 index 00000000000..3c90116037f --- /dev/null +++ b/operator/examples/nessie-inmemory.yaml @@ -0,0 +1,45 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-inmemory +spec: + size: 1 + logLevel: INFO + deployment: + image: + repository: ghcr.io/projectnessie/nessie + serviceAccount: + create: true + name: nessie-inmemory-sa + versionStore: + type: InMemory + authentication: + enabled: true + oidcAuthServerUrl: http://localhost:8080/auth/realms/nessie + oidcClientId: quarkus-app + authorization: + enabled: true + rules: + allowViewingBranch: op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + telemetry: + enabled: true + endpoint: http://localhost:14268/api/traces + sample: "1.0" + attributes: + foo: "bar" + extraEnv: + - name: QUARKUS_PROFILE + value: "prod" + remoteDebug: + enabled: true + port: 5009 + jvmOptions: + - -XX:+UnlockDiagnosticVMOptions + - -XX:+PrintFlagsFinal + advancedConfig: + nessie.server.default-branch: my-branch + nessie.version.store.persist.repository-id: my-repository + quarkus: + log: + console.format: "%d{HH:mm:ss} %s%e%n" + category."org.projectnessie".level: "DEBUG" diff --git a/operator/examples/nessie-postgres.yaml b/operator/examples/nessie-postgres.yaml new file mode 100644 index 00000000000..be7b22ab168 --- /dev/null +++ b/operator/examples/nessie-postgres.yaml @@ -0,0 +1,19 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-postgres +spec: + size: 3 + logLevel: INFO + deployment: + image: + repository: ghcr.io/projectnessie/nessie + versionStore: + type: Jdbc + jdbc: + # helm install nessie-postgres oci://registry-1.docker.io/bitnamicharts/postgresql + url: jdbc:postgresql://nessie-postgres-postgresql:5432/nessiedb?currentSchema=nessie + username: postgres + password: + secret: postgres-creds + key: postgres_password diff --git a/operator/examples/nessie-rocks.yaml b/operator/examples/nessie-rocks.yaml new file mode 100644 index 00000000000..8f1e21284b0 --- /dev/null +++ b/operator/examples/nessie-rocks.yaml @@ -0,0 +1,23 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-rocks +spec: + size: 1 + logLevel: INFO + deployment: + image: + repository: ghcr.io/projectnessie/nessie + versionStore: + type: RocksDb + rocksDb: + storageClassName: standard + storageSize: 64Mi + # Access nessie: + # curl -H "Host: nessie-rocks.example.com" -k https://$(minikube ip)/api/v2/config + ingress: + enabled: true + rules: + - host: nessie-rocks.example.com + paths: + - / diff --git a/operator/examples/nessie-simple.yaml b/operator/examples/nessie-simple.yaml new file mode 100644 index 00000000000..97a60786fdb --- /dev/null +++ b/operator/examples/nessie-simple.yaml @@ -0,0 +1,6 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-simple + namespace: nessie-ns +spec: {} diff --git a/operator/src/intTest/java/org/projectnessie/operator/reconciler/AbstractReconcilerIntegrationTests.java b/operator/src/intTest/java/org/projectnessie/operator/reconciler/AbstractReconcilerIntegrationTests.java new file mode 100644 index 00000000000..1cbf86d6b43 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/AbstractReconcilerIntegrationTests.java @@ -0,0 +1,97 @@ +/* + * 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; + +import io.fabric8.kubernetes.api.model.EventList; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder; +import io.fabric8.kubernetes.api.model.Pod; +import java.time.Duration; +import org.junit.jupiter.api.AfterEach; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager.Kubectl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractReconcilerIntegrationTests + extends AbstractReconcilerTests { + + private static final Logger LOGGER = + LoggerFactory.getLogger(AbstractReconcilerIntegrationTests.class); + + protected Kubectl kubectl; + + @Override + protected Duration pollInterval() { + return Duration.ofSeconds(5); + } + + @Override + protected Duration timeout() { + return Duration.ofMinutes(5); + } + + @Override + protected void waitForPrimaryReady() { + LOGGER.info( + "Waiting for {} {} to be ready", primary.getSingular(), primary.getMetadata().getName()); + kubectl.waitUntil(primary, namespace.getMetadata().getName(), "Ready", timeout()); + } + + protected EventList getPrimaryEventList() { + return client + .v1() + .events() + .inNamespace(namespace.getMetadata().getName()) + .withInvolvedObject( + new ObjectReferenceBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .withUid(primary.getMetadata().getUid()) + .build()) + .list(); + } + + @Override + protected void dumpNamespace() { + if (client != null) { + LOGGER.error("Dumping namespace {}", namespace.getMetadata().getName()); + try { + for (Pod pod : list(client.pods())) { + LOGGER.error("{}", client.getKubernetesSerialization().asYaml(pod)); + LOGGER.error("Logs:\n{}", kubectl.logs(pod, false)); + LOGGER.error("Previous logs:\n{}", kubectl.logs(pod, true)); + } + } catch (Exception e) { + LOGGER.error("Failed to dump namespace: {}", e.getMessage()); + } + } + } + + @AfterEach + protected void clearNamespace() { + try { + if (primary != null) { + client.resource(primary).delete(); + } + if (kubectl != null && namespace != null) { + LOGGER.info("Deleting all resources in namespace {}", namespace.getMetadata().getName()); + kubectl.deleteAll(namespace.getMetadata().getName(), timeout()); + } + } finally { + client.resource(primary).withGracePeriod(0).delete(); + } + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/AbstractNessieReconcilerIntegrationTests.java b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/AbstractNessieReconcilerIntegrationTests.java new file mode 100644 index 00000000000..0eacac335e0 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/AbstractNessieReconcilerIntegrationTests.java @@ -0,0 +1,174 @@ +/* + * 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; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import org.junit.jupiter.api.AfterEach; +import org.projectnessie.client.NessieClientBuilder; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.http.HttpAuthentication; +import org.projectnessie.client.http.NessieHttpClientBuilder; +import org.projectnessie.error.NessieConflictException; +import org.projectnessie.error.NessieNotFoundException; +import org.projectnessie.model.Branch; +import org.projectnessie.model.CommitMeta; +import org.projectnessie.model.CommitResponse; +import org.projectnessie.model.ContentKey; +import org.projectnessie.model.IcebergTable; +import org.projectnessie.model.NessieConfiguration; +import org.projectnessie.model.Operation.Put; +import org.projectnessie.operator.reconciler.AbstractReconcilerIntegrationTests; +import org.projectnessie.operator.reconciler.nessie.dependent.ConfigMapDependent; +import org.projectnessie.operator.reconciler.nessie.dependent.DeploymentDependent; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager.NessieUri; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager.PrometheusUri; + +public abstract class AbstractNessieReconcilerIntegrationTests + extends AbstractReconcilerIntegrationTests { + + private static final String NESSIE_INGRESS_HOST = "nessie.example.com"; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @NessieUri protected URI nessieUri; + + @PrometheusUri protected URI prometheusUri; + + protected NessieApiV2 nessieClient; + + protected Deployment overrideConfigChecksum(Deployment deployment) { + return deployment + .edit() + .editSpec() + .editTemplate() + .editMetadata() + .addToAnnotations( + DeploymentDependent.CONFIG_CHECKSUM_ANNOTATION, + ConfigMapDependent.configChecksum(primary)) + .endMetadata() + .endTemplate() + .endSpec() + .build(); + } + + @Override + protected void setUpFunctionalTest() { + nessieClient = nessieNodePortClient(null).build(NessieApiV2.class); + } + + protected NessieHttpClientBuilder nessieNodePortClient(HttpAuthentication authentication) { + return ((NessieHttpClientBuilder) NessieClientBuilder.createClientBuilderFromSystemSettings()) + .withUri(nessieUri) + .withAuthentication(authentication) + .withApiCompatibilityCheck(false); + } + + protected NessieHttpClientBuilder nessieIngressClient() { + return nessieNodePortClient(null) + .addRequestFilter(ctx -> ctx.putHeader("Host", NESSIE_INGRESS_HOST)); + } + + @Override + protected void functionalTest() throws Exception { + checkNessieOperational(); + if (primary.getSpec().telemetry().enabled()) { + checkTelemetry(); + } + if (primary.getSpec().monitoring().enabled()) { + checkServiceStatus(); + } + } + + protected void checkNessieOperational() throws NessieNotFoundException, NessieConflictException { + NessieConfiguration config = nessieClient.getConfig(); + Branch branch = (Branch) nessieClient.getReference().refName(config.getDefaultBranch()).get(); + String tableName = "table-" + System.nanoTime(); + ContentKey key = ContentKey.of(tableName); + IcebergTable table = IcebergTable.of("irrelevant", 1, 2, 3, 4); + CommitResponse response = + nessieClient + .commitMultipleOperations() + .branch(branch) + .commitMeta(CommitMeta.fromMessage("Add " + tableName)) + .operation(Put.of(key, table)) + .commitWithResponse(); + assertThat(response.getAddedContents()).isNotNull(); + assertThat(response.getAddedContents().size()).isOne(); + } + + protected void checkServiceStatus() throws IOException { + JsonNode metrics = + OBJECT_MAPPER.readValue( + prometheusUri + .resolve( + "/api/v1/query?query=" + + URLEncoder.encode( + "nessie_versionstore_request_total{application=\"Nessie\",method=\"commit\"}", + UTF_8)) + .toURL(), + JsonNode.class); + assertThat(metrics.get("status").asText()).isEqualTo("success"); + JsonNode result = metrics.get("data").get("result"); + // A typical result looks like: + // { + // "metric":{"__name__":"...","container":"nessie","endpoint":"nessie-test-mgmt", ... }, + // "value":[1.70956248969E9,"1"] + // } + assertThat(result) + .isNotEmpty() + .anySatisfy(r -> assertThat(r.get("value").get(1).asInt()).isGreaterThanOrEqualTo(1)); + } + + protected void checkTelemetry() { + // The otel-collector pod should have received traces, and it is configured with the + // debug exporter, so we can check its logs for traces. + Pod pod = client.pods().inNamespace("otel-collector").list().getItems().get(0); + String logs = kubectl.logs(pod.getMetadata().getName(), "otel-collector"); + assertThat(logs) + .contains("ObservingPersist.fetchReference") + .contains("service.name: Str(nessie-test-custom)"); + } + + @Override + protected void assertResourcesDeleted() { + assertThat(get(client.serviceAccounts(), "nessie-test")).isNull(); + assertThat(get(client.apps().deployments(), "nessie-test")).isNull(); + assertThat(get(client.services(), "nessie-test")).isNull(); + assertThat(get(client.services(), "nessie-test-mgmt")).isNull(); + assertThat(get(client.network().v1().ingresses(), "nessie-test")).isNull(); + assertThat(get(client.monitoring().serviceMonitors(), "nessie-test")).isNull(); + assertThat(getPrimaryEventList().getItems()).isEmpty(); + assertThat(client.resource(primary).get()).isNull(); + } + + @AfterEach + protected void closeNessieClient() { + if (nessieClient != null) { + nessieClient.close(); + nessieClient = null; + } + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerBigTable.java b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerBigTable.java new file mode 100644 index 00000000000..3479cb29f10 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerBigTable.java @@ -0,0 +1,124 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceMonitor; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; +import static org.projectnessie.operator.testinfra.BigTableContainerLifecycleManager.BIGTABLE_PORT; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.fabric8.kubernetes.api.model.Pod; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.testinfra.BigTableContainerLifecycleManager; +import org.projectnessie.operator.testinfra.BigTableContainerLifecycleManager.BigTableHost; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager; + +@QuarkusIntegrationTest +@QuarkusTestResource( + value = BigTableContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +@QuarkusTestResource( + value = K3sContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true, + initArgs = { + @ResourceArg(name = "monitoring", value = "true"), + }) +class ITNessieReconcilerBigTable extends AbstractNessieReconcilerIntegrationTests { + + private static final String PREFIX = "/org/projectnessie/operator/inttests/fixtures/bigtable/"; + + @BigTableHost private String bigTableHost; + + @BeforeEach + void createRequiredResources() { + create(client.secrets(), PREFIX + "secret.yaml"); + } + + @Override + protected Nessie newPrimary() { + Nessie nessie = load(client.resources(Nessie.class), PREFIX + "nessie.yaml"); + ((ObjectNode) nessie.getSpec().advancedConfig()) + .put("nessie.version.store.persist.bigtable.emulator-host", bigTableHost) + .put("nessie.version.store.persist.bigtable.emulator-port", BIGTABLE_PORT); + return nessie; + } + + @Override + protected void assertResourcesCreated() { + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test"), + "nessie.version.store.persist.bigtable.emulator-host", + bigTableHost); + checkDeployment( + overrideConfigChecksum(load(client.apps().deployments(), PREFIX + "deployment.yaml")), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkServiceMonitor( + load(client.monitoring().serviceMonitors(), PREFIX + "service-monitor.yaml"), + get(client.monitoring().serviceMonitors(), "nessie-test")); + checkEvents( + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + CreatingServiceMonitor, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.network().v1().ingresses()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + } + + @Override + protected void functionalTest() throws Exception { + super.functionalTest(); + checkRemoteDebugAndJvmOptions(); + } + + private void checkRemoteDebugAndJvmOptions() { + Pod pod = client.pods().inNamespace(namespace.getMetadata().getName()).list().getItems().get(0); + String logs = kubectl.logs(pod.getMetadata().getName(), pod.getMetadata().getNamespace()); + assertThat(logs) + .contains("Listening for transport dt_socket at address: 5009") + .contains("-XX:+PrintFlagsFinal"); + } + + @Override + protected void assertResourcesDeleted() { + super.assertResourcesDeleted(); + assertThat(get(client.secrets(), "nessie-db-credentials")).isNotNull(); + assertThat(get(client.serviceAccounts(), "default")).isNotNull(); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerCassandra.java b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerCassandra.java new file mode 100644 index 00000000000..daec5674cdc --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerCassandra.java @@ -0,0 +1,111 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceAccount; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.testinfra.CassandraContainerLifecycleManager; +import org.projectnessie.operator.testinfra.CassandraContainerLifecycleManager.CassandraContactPoint; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager; + +@QuarkusIntegrationTest +@QuarkusTestResource( + value = CassandraContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +@QuarkusTestResource( + value = K3sContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true, + initArgs = { + @ResourceArg(name = "telemetry", value = "true"), + }) +class ITNessieReconcilerCassandra extends AbstractNessieReconcilerIntegrationTests { + + private static final String PREFIX = "/org/projectnessie/operator/inttests/fixtures/cassandra/"; + + @CassandraContactPoint private String contactPoint; + + @BeforeEach + void createRequiredResources() { + create(client.secrets(), PREFIX + "secret.yaml"); + } + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml") + .edit() + .editSpec() + .editVersionStore() + .editCassandra() + .withContactPoints(contactPoint) + .endCassandra() + .endVersionStore() + .endSpec() + .build(); + } + + @Override + protected void assertResourcesCreated() { + checkServiceAccount( + load(client.serviceAccounts(), PREFIX + "service-account.yaml"), + get(client.serviceAccounts(), "nessie-test")); + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test"), + "quarkus.cassandra.contact-points", + contactPoint); + checkDeployment( + overrideConfigChecksum(load(client.apps().deployments(), PREFIX + "deployment.yaml")), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkEvents( + CreatingServiceAccount, + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.network().v1().ingresses()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.monitoring().serviceMonitors()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + } + + @Override + protected void assertResourcesDeleted() { + super.assertResourcesDeleted(); + assertThat(get(client.secrets(), "nessie-db-credentials")).isNotNull(); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerDynamo.java b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerDynamo.java new file mode 100644 index 00000000000..60caf89b451 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerDynamo.java @@ -0,0 +1,157 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingIngress; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceAccount; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ManagedFieldsEntry; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.ResourceArg; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.testinfra.DynamoContainerLifecycleManager; +import org.projectnessie.operator.testinfra.DynamoContainerLifecycleManager.DynamoEndpoint; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager; + +@QuarkusIntegrationTest +@QuarkusTestResource( + value = DynamoContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +@QuarkusTestResource( + value = K3sContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true, + initArgs = { + @ResourceArg(name = "ingress", value = "true"), + }) +class ITNessieReconcilerDynamo extends AbstractNessieReconcilerIntegrationTests { + + private static final String PREFIX = "/org/projectnessie/operator/inttests/fixtures/dynamo/"; + + @DynamoEndpoint private String dynamoEndpoint; + + @BeforeEach + void createRequiredResources() { + create(client.secrets(), PREFIX + "secret.yaml"); + } + + @Override + protected Nessie newPrimary() { + Nessie nessie = load(client.resources(Nessie.class), PREFIX + "nessie.yaml"); + ((ObjectNode) nessie.getSpec().advancedConfig()) + .put("quarkus.dynamodb.endpoint-override", dynamoEndpoint); + return nessie; + } + + @Override + protected void assertResourcesCreated() { + checkServiceAccount( + load(client.serviceAccounts(), PREFIX + "service-account.yaml"), + get(client.serviceAccounts(), "nessie-test")); + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test"), + "quarkus.dynamodb.endpoint-override", + dynamoEndpoint); + emulateSideCarInjection(); + checkDeployment( + overrideConfigChecksum(load(client.apps().deployments(), PREFIX + "deployment.yaml")), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkIngress( + load(client.network().v1().ingresses(), PREFIX + "ingress.yaml"), + get(client.network().v1().ingresses(), "nessie-test")); + checkEvents( + CreatingServiceAccount, + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + CreatingIngress, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.monitoring().serviceMonitors()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + } + + @Override + protected void setUpFunctionalTest() { + nessieClient = nessieIngressClient().build(NessieApiV2.class); + } + + @Override + protected void assertResourcesDeleted() { + super.assertResourcesDeleted(); + assertThat(get(client.secrets(), "nessie-dynamo-credentials")).isNotNull(); + } + + private void emulateSideCarInjection() { + Deployment actual = get(client.apps().deployments(), "nessie-test"); + assertThat(actual).isNotNull(); + if (actual.getSpec().getTemplate().getSpec().getInitContainers().isEmpty()) { + Deployment desired = + new DeploymentBuilder() + .withNewMetadata() + .withName("nessie-test") + .withNamespace(namespace.getMetadata().getName()) + .withResourceVersion(actual.getMetadata().getResourceVersion()) + .endMetadata() + .withNewSpec() + .withNewTemplate() + .withNewSpec() + .withInitContainers( + new ContainerBuilder() + .withName("sidecar") + .withImage("k8s.gcr.io/pause") + .withImagePullPolicy("IfNotPresent") + .build()) + .endSpec() + .endTemplate() + .endSpec() + .build(); + Deployment updated = + client + .resource(desired) + .fieldManager("sidecar-injector") + .forceConflicts() + .serverSideApply(); + assertThat(updated.getMetadata().getManagedFields()) + .extracting(ManagedFieldsEntry::getManager) + .contains("nessie-controller", "sidecar-injector"); + } + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerJdbc.java b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerJdbc.java new file mode 100644 index 00000000000..d67071683c6 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerJdbc.java @@ -0,0 +1,131 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.MAP; +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingHPA; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceAccount; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.fabric8.kubernetes.api.model.ManagedFieldsEntry; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager; +import org.projectnessie.operator.testinfra.PostgresContainerLifecycleManager; +import org.projectnessie.operator.testinfra.PostgresContainerLifecycleManager.JdbcUrl; + +@QuarkusIntegrationTest +@QuarkusTestResource( + value = PostgresContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +@QuarkusTestResource( + value = K3sContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +class ITNessieReconcilerJdbc extends AbstractNessieReconcilerIntegrationTests { + + private static final String PREFIX = "/org/projectnessie/operator/inttests/fixtures/jdbc/"; + + @JdbcUrl private String jdbcUrl; + + @BeforeEach + void createRequiredResources() { + create(client.secrets(), PREFIX + "secret.yaml"); + } + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml") + .edit() + .editSpec() + .editVersionStore() + .editJdbc() + .withUrl(jdbcUrl) + .endJdbc() + .endVersionStore() + .endSpec() + .build(); + } + + @Override + protected void assertResourcesCreated() { + checkServiceAccount( + load(client.serviceAccounts(), PREFIX + "service-account.yaml"), + get(client.serviceAccounts(), "nessie-test-custom-service-account")); + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test"), + "quarkus.datasource.postgresql.jdbc.url", + jdbcUrl); + checkDeployment( + overrideConfigChecksum(load(client.apps().deployments(), PREFIX + "deployment.yaml")), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkAutoscaler( + load(client.autoscaling().v2().horizontalPodAutoscalers(), PREFIX + "autoscaler.yaml"), + get(client.autoscaling().v2().horizontalPodAutoscalers(), "nessie-test")); + checkReplicasManagedByHPA(); + checkEvents( + CreatingServiceAccount, + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + CreatingHPA, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.network().v1().ingresses()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.monitoring().serviceMonitors()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + } + + private void checkReplicasManagedByHPA() { + Deployment actual = get(client.apps().deployments(), "nessie-test"); + assertThat(actual).isNotNull(); + assertThat(actual.getSpec().getReplicas()).isEqualTo(2); + ManagedFieldsEntry fields = + actual.getMetadata().getManagedFields().stream() + .filter(m -> m.getManager().equals("nessie-controller")) + .findFirst() + .orElseThrow(); + assertThat(fields.getFieldsV1().getAdditionalProperties()).containsKey("f:spec"); + assertThat(fields.getFieldsV1().getAdditionalProperties().get("f:spec")) + .asInstanceOf(MAP) + .doesNotContainKey("f:replicas"); + } + + @Override + protected void assertResourcesDeleted() { + super.assertResourcesDeleted(); + assertThat(get(client.secrets(), "nessie-db-credentials")).isNotNull(); + assertThat(get(client.serviceAccounts(), "nessie-test-custom-service-account")).isNull(); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerMongo.java b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerMongo.java new file mode 100644 index 00000000000..d88e5678b48 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerMongo.java @@ -0,0 +1,106 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; +import static org.projectnessie.operator.testinfra.MongoContainerLifecycleManager.DATABASE_NAME; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.junit.jupiter.api.BeforeEach; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager; +import org.projectnessie.operator.testinfra.MongoContainerLifecycleManager; +import org.projectnessie.operator.testinfra.MongoContainerLifecycleManager.MongoConnectionString; + +@QuarkusIntegrationTest +@QuarkusTestResource( + value = MongoContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +@QuarkusTestResource( + value = K3sContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +class ITNessieReconcilerMongo extends AbstractNessieReconcilerIntegrationTests { + + private static final String PREFIX = "/org/projectnessie/operator/inttests/fixtures/mongo/"; + + @MongoConnectionString private String connectionString; + + @BeforeEach + void createRequiredResources() { + create(client.secrets(), PREFIX + "secret.yaml"); + create(client.serviceAccounts(), PREFIX + "service-account.yaml"); + } + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml") + .edit() + .editSpec() + .editVersionStore() + .editMongoDb() + .withConnectionString(connectionString) + .withDatabase(DATABASE_NAME) + .endMongoDb() + .endVersionStore() + .endSpec() + .build(); + } + + @Override + protected void assertResourcesCreated() { + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test"), + "quarkus.mongodb.connection-string", + connectionString); + checkDeployment( + overrideConfigChecksum(load(client.apps().deployments(), PREFIX + "deployment.yaml")), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkEvents( + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.network().v1().ingresses()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.monitoring().serviceMonitors()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + } + + @Override + protected void assertResourcesDeleted() { + super.assertResourcesDeleted(); + assertThat(get(client.secrets(), "nessie-db-credentials")).isNotNull(); + assertThat(get(client.serviceAccounts(), "nessie-test-custom-service-account")).isNotNull(); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerRocks.java b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerRocks.java new file mode 100644 index 00000000000..1db641965f0 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerRocks.java @@ -0,0 +1,116 @@ +/* + * 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; + +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingPersistentVolumeClaim; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceAccount; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import java.net.URI; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.client.auth.oauth2.OAuth2AuthenticationProvider; +import org.projectnessie.client.auth.oauth2.OAuth2AuthenticatorConfig; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.testinfra.K3sContainerLifecycleManager; +import org.projectnessie.operator.testinfra.KeycloakContainerLifecycleManager; +import org.projectnessie.operator.testinfra.KeycloakContainerLifecycleManager.ExternalRealmUri; +import org.projectnessie.operator.testinfra.KeycloakContainerLifecycleManager.InternalRealmUri; +import org.projectnessie.testing.keycloak.CustomKeycloakContainer; + +@QuarkusIntegrationTest +@QuarkusTestResource( + value = KeycloakContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +@QuarkusTestResource( + value = K3sContainerLifecycleManager.class, + parallel = true, + restrictToAnnotatedClass = true) +class ITNessieReconcilerRocks extends AbstractNessieReconcilerIntegrationTests { + + private static final String PREFIX = "/org/projectnessie/operator/inttests/fixtures/rocks/"; + + @InternalRealmUri private URI keycloakInternalRealmUri; + @ExternalRealmUri private URI keycloakExternalRealmUri; + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml") + .edit() + .editSpec() + .editAuthentication() + .withOidcAuthServerUrl(String.valueOf(keycloakInternalRealmUri)) + .endAuthentication() + .endSpec() + .build(); + } + + @Override + protected void assertResourcesCreated() { + checkServiceAccount( + load(client.serviceAccounts(), PREFIX + "service-account.yaml"), + get(client.serviceAccounts(), "nessie-test-sa")); + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test"), + "quarkus.oidc.auth-server-url", + keycloakInternalRealmUri); + checkPvc( + load(client.persistentVolumeClaims(), PREFIX + "pvc.yaml"), + get(client.persistentVolumeClaims(), "nessie-test")); + checkDeployment( + overrideConfigChecksum(load(client.apps().deployments(), PREFIX + "deployment.yaml")), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkEvents( + CreatingServiceAccount, + CreatingPersistentVolumeClaim, + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + ReconcileSuccess); + checkNotCreated(client.network().v1().ingresses()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + checkNotCreated(client.monitoring().serviceMonitors()); + } + + @Override + protected void setUpFunctionalTest() { + OAuth2AuthenticatorConfig config = + OAuth2AuthenticatorConfig.builder() + .issuerUrl(keycloakExternalRealmUri) + .clientId(KeycloakContainerLifecycleManager.CLIENT_ID) + .clientSecret(CustomKeycloakContainer.CLIENT_SECRET) + .addScopes("email", "profile") + .build(); + nessieClient = + nessieNodePortClient(OAuth2AuthenticationProvider.create(config)).build(NessieApiV2.class); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/testinfra/AbstractContainerLifecycleManager.java b/operator/src/intTest/java/org/projectnessie/operator/testinfra/AbstractContainerLifecycleManager.java new file mode 100644 index 00000000000..4b27acc24d8 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/AbstractContainerLifecycleManager.java @@ -0,0 +1,89 @@ +/* + * 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.testinfra; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import jakarta.annotation.Nullable; +import java.util.Map; +import java.util.Objects; +import org.projectnessie.nessie.testing.containerspec.ContainerSpecHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.Network.NetworkImpl; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +public abstract class AbstractContainerLifecycleManager> + implements QuarkusTestResourceLifecycleManager { + + protected C container; + protected String inDockerIpAddress; + + protected AbstractContainerLifecycleManager() {} + + @Override + public Map start() { + Logger logger = LoggerFactory.getLogger(getClass()); + container = createContainer(); + container + .withNetwork(Network.SHARED) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withStartupAttempts(3); + container.start(); + inDockerIpAddress = + Objects.requireNonNull( + getInDockerIpAddress(), "could not determine container's in-docker IP address"); + return quarkusConfig(); + } + + protected Map quarkusConfig() { + return Map.of(); + } + + protected DockerImageName dockerImage(String name) { + return ContainerSpecHelper.builder() + .name(name) + .containerClass(this.getClass()) + .build() + .dockerImageName(null); + } + + protected abstract C createContainer(); + + /** + * The "in-docker" IP address of the container. This IP address is addressable from a deployment + * running in the K3s container, contrary to the address returned by `container.getHost()` or any + * of the network aliases defined for the container. + */ + @Nullable + protected String getInDockerIpAddress() { + return container + .getCurrentContainerInfo() + .getNetworkSettings() + .getNetworks() + .get(((NetworkImpl) Network.SHARED).getName()) + .getIpAddress(); + } + + @Override + public void stop() { + if (container != null) { + container.stop(); + } + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/testinfra/BigTableContainerLifecycleManager.java b/operator/src/intTest/java/org/projectnessie/operator/testinfra/BigTableContainerLifecycleManager.java new file mode 100644 index 00000000000..64565582bb4 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/BigTableContainerLifecycleManager.java @@ -0,0 +1,57 @@ +/* + * 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.testinfra; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.Annotated; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.MatchesType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +public class BigTableContainerLifecycleManager + extends AbstractContainerLifecycleManager> { + + public static final int BIGTABLE_PORT = 8086; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface BigTableHost {} + + @SuppressWarnings("resource") + @Override + protected GenericContainer createContainer() { + return new GenericContainer<>(dockerImage("bigtable")) + .withExposedPorts(BIGTABLE_PORT) + .withCommand( + "gcloud", + "beta", + "emulators", + "bigtable", + "start", + "--verbosity=info", + "--host-port=0.0.0.0:" + BIGTABLE_PORT) + .waitingFor(Wait.forLogMessage(".*Bigtable emulator running.*", 1)); + } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields( + inDockerIpAddress, new MatchesType(String.class).and(new Annotated(BigTableHost.class))); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/testinfra/CassandraContainerLifecycleManager.java b/operator/src/intTest/java/org/projectnessie/operator/testinfra/CassandraContainerLifecycleManager.java new file mode 100644 index 00000000000..0f24120ee95 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/CassandraContainerLifecycleManager.java @@ -0,0 +1,63 @@ +/* + * 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.testinfra; + +import static org.testcontainers.containers.CassandraContainer.CQL_PORT; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.Annotated; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.MatchesType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.testcontainers.containers.CassandraContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +public class CassandraContainerLifecycleManager + extends AbstractContainerLifecycleManager> { + + static { + // the init script is executed with driver 3.x, epoll won't be available + System.setProperty("com.datastax.driver.FORCE_NIO", "true"); + } + + private static final String JVM_OPTS_TEST = + "-Dcassandra.skip_wait_for_gossip_to_settle=0 " + + "-Dcassandra.num_tokens=1 " + + "-Dcassandra.initial_token=0"; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface CassandraContactPoint {} + + @SuppressWarnings("resource") + @Override + protected CassandraContainer createContainer() { + return new CassandraContainer<>(dockerImage("cassandra").asCompatibleSubstituteFor("cassandra")) + .withInitScript("org/projectnessie/operator/inttests/fixtures/cassandra/init.cql") + .withEnv("JVM_OPTS", JVM_OPTS_TEST) + .waitingFor(Wait.forLogMessage(".*Startup complete.*", 1)); + } + + @Override + public void inject(TestInjector testInjector) { + super.inject(testInjector); + String contactPoint = inDockerIpAddress + ":" + CQL_PORT; + testInjector.injectIntoFields( + contactPoint, + new MatchesType(String.class).and(new Annotated(CassandraContactPoint.class))); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/testinfra/DynamoContainerLifecycleManager.java b/operator/src/intTest/java/org/projectnessie/operator/testinfra/DynamoContainerLifecycleManager.java new file mode 100644 index 00000000000..3724bc869d5 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/DynamoContainerLifecycleManager.java @@ -0,0 +1,52 @@ +/* + * 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.testinfra; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.Annotated; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.MatchesType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +public class DynamoContainerLifecycleManager + extends AbstractContainerLifecycleManager> { + + public static final int DYNAMODB_PORT = 8000; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface DynamoEndpoint {} + + @SuppressWarnings("resource") + @Override + protected GenericContainer createContainer() { + return new GenericContainer<>(dockerImage("dynamo")) + .withExposedPorts(DYNAMODB_PORT) + .withCommand("-jar", "DynamoDBLocal.jar", "-inMemory", "-sharedDb") + .waitingFor(Wait.forLogMessage(".*Initializing DynamoDB Local.*", 1)); + } + + @Override + public void inject(TestInjector testInjector) { + super.inject(testInjector); + String endpoint = String.format("http://%s:%d", inDockerIpAddress, DYNAMODB_PORT); + testInjector.injectIntoFields( + endpoint, new MatchesType(String.class).and(new Annotated(DynamoEndpoint.class))); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/testinfra/K3sContainerLifecycleManager.java b/operator/src/intTest/java/org/projectnessie/operator/testinfra/K3sContainerLifecycleManager.java new file mode 100644 index 00000000000..baa5edc663b --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/K3sContainerLifecycleManager.java @@ -0,0 +1,506 @@ +/* + * 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.testinfra; + +import static org.assertj.core.api.Fail.fail; +import static org.awaitility.Awaitility.await; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; +import io.fabric8.openshift.client.OpenShiftClient; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.AnnotatedAndMatchesType; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.MatchesType; +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.intellij.lang.annotations.Language; +import org.projectnessie.api.NessieVersion; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Container.ExecResult; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.k3s.K3sContainer; + +public class K3sContainerLifecycleManager extends AbstractContainerLifecycleManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(K3sContainerLifecycleManager.class); + + private static final int NESSIE_INGRESS_PORT = 80; + private static final int PROMETHEUS_NODE_PORT = 30090; + private static final int NESSIE_NODE_PORT = 30120; + + @Language("YAML") + private static final String TRAEFIK_HELM_CHART = + """ + apiVersion: helm.cattle.io/v1 + kind: HelmChartConfig + metadata: + name: traefik + namespace: kube-system + spec: + valuesContent: |- + ingressRoute: + dashboard: + enabled: false + healthcheck: + enabled: false + providers: + kubernetesCRD: + enabled: false + metrics: + addInternals: false + prometheus: null + resources: + requests: + cpu: "300m" + memory: "150Mi" + limits: + cpu: "300m" + memory: "150Mi" + livenessProbe: + initialDelaySeconds: 2 + failureThreshold: 30 + periodSeconds: 1 + readinessProbe: + initialDelaySeconds: 2 + failureThreshold: 30 + periodSeconds: 1 + """; + + @Language("YAML") + private static final String PROMETHEUS_HELM_CHART = + """ + apiVersion: helm.cattle.io/v1 + kind: HelmChart + metadata: + name: prometheus + namespace: kube-system + spec: + repo: https://charts.bitnami.com/bitnami + chart: kube-prometheus + targetNamespace: prometheus + createNamespace: true + valuesContent: |- + prometheus.serviceMonitorSelector: + app.kubernetes.io/component: nessie + set: + alertmanager.enabled: "false" + blackboxExporter.enabled: "false" + coreDns.enabled: "false" + coreDns.service.enabled: "false" + exporters.node-exporter.enabled: "false" + exporters.kube-state-metrics.enabled: "false" + kubelet.enabled: "false" + kubeApiServer.enabled: "false" + kubeControllerManager.enabled: "false" + kubeControllerManager.service.enabled: "false" + kubeProxy.enabled: "false" + kubeProxy.service.enabled: "false" + kubeScheduler.enabled: "false" + kubeScheduler.service.enabled: "false" + operator.serviceMonitor.enabled: "false" + operator.kubeletService.enabled: "false" + prometheus.configReloader.service.enabled: "false" + prometheus.scrapeInterval: "1s" + prometheus.serviceMonitor.enabled: "false" + prometheus.service.type: NodePort + prometheus.service.nodePorts.http: %d + """; + + @Language("YAML") + private static final String COLLECTOR_HELM_CHART = + """ + apiVersion: helm.cattle.io/v1 + kind: HelmChart + metadata: + name: otel-collector + namespace: kube-system + spec: + repo: https://open-telemetry.github.io/opentelemetry-helm-charts + chart: opentelemetry-collector + targetNamespace: otel-collector + createNamespace: true + valuesContent: |- + mode: deployment + image: + repository: "otel/opentelemetry-collector-k8s" + ports: + jaeger-compact: + enabled: false + jaeger-thrift: + enabled: false + jaeger-grpc: + enabled: false + zipkin: + enabled: false + config: + receivers: + jaeger: null + prometheus: null + zipkin: null + exporters: + debug: + verbosity: detailed + sampling_initial: 1 + sampling_thereafter: 1 + service: + pipelines: + traces: + exporters: + - debug + receivers: + - otlp + logs: null + metrics: null + """; + + @Language("Shell Script") + private static final String IMAGE_IMPORT_SCRIPT = + """ + #!/usr/bin/env bash + set -e + 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 - + """; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface NessieUri {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface PrometheusUri {} + + private URI nessieUri; + private URI prometheusUri; + + private OpenShiftClient k8sClient; + + private boolean monitoring; + private boolean ingress; + private boolean telemetry; + private boolean waitForComponents; + + @Override + public void init(Map initArgs) { + monitoring = Boolean.parseBoolean(initArgs.getOrDefault("monitoring", "false")); + ingress = Boolean.parseBoolean(initArgs.getOrDefault("ingress", "false")); + telemetry = Boolean.parseBoolean(initArgs.getOrDefault("telemetry", "false")); + waitForComponents = Boolean.parseBoolean(initArgs.getOrDefault("waitForComponents", "false")); + } + + @Override + protected K3sContainer createContainer() { + K3sContainer container = + new K3sContainer(dockerImage("k3s").asCompatibleSubstituteFor("rancher/k3s")); + List exposedPorts = new ArrayList<>(); + if (monitoring) { + container.withCopyToContainer( + Transferable.of(PROMETHEUS_HELM_CHART.formatted(PROMETHEUS_NODE_PORT)), + "/var/lib/rancher/k3s/server/manifests/prometheus.yaml"); + exposedPorts.add(PROMETHEUS_NODE_PORT); + } + if (ingress) { + container.withCopyToContainer( + Transferable.of(TRAEFIK_HELM_CHART), + "/var/lib/rancher/k3s/server/manifests/traefik-config.yaml"); + // override default command to enable Traefik + container.setCommand("server", "--tls-san=" + container.getHost()); + exposedPorts.add(NESSIE_INGRESS_PORT); + } else { + exposedPorts.add(NESSIE_NODE_PORT); + } + if (telemetry) { + container.withCopyToContainer( + Transferable.of(COLLECTOR_HELM_CHART), + "/var/lib/rancher/k3s/server/manifests/otel-collector.yaml"); + } + container.addExposedPorts(exposedPorts.stream().mapToInt(Integer::intValue).toArray()); + return container; + } + + @Override + protected Map quarkusConfig() { + loadNessieImage(); + setUpK8sClient(); + installCrds(); + setUpUris(); + Config config = k8sClient.getConfiguration(); + return Map.of( + "quarkus.kubernetes-client.api-server-url", + config.getMasterUrl(), + "quarkus.kubernetes-client.ca-cert-data", + config.getCaCertData(), + "quarkus.kubernetes-client.client-cert-data", + config.getClientCertData(), + "quarkus.kubernetes-client.client-key-data", + config.getClientKeyData(), + "quarkus.kubernetes-client.client-key-passphrase", + config.getClientKeyPassphrase(), + "quarkus.kubernetes-client.client-key-algo", + config.getClientKeyAlgo(), + "quarkus.kubernetes-client.namespace", + "default"); + } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(new Kubectl(), new MatchesType(Kubectl.class)); + testInjector.injectIntoFields(k8sClient, new MatchesType(OpenShiftClient.class)); + testInjector.injectIntoFields( + nessieUri, new AnnotatedAndMatchesType(NessieUri.class, URI.class)); + testInjector.injectIntoFields( + prometheusUri, new AnnotatedAndMatchesType(PrometheusUri.class, URI.class)); + } + + @Override + public void stop() { + if (k8sClient != null) { + k8sClient.close(); + } + super.stop(); + } + + private void loadNessieImage() { + LOGGER.info("Importing Nessie server image into K3S node..."); + ProcessBuilder pb = new ProcessBuilder("bash", "-c", IMAGE_IMPORT_SCRIPT); + pb.environment().put("NESSIE_VERSION", NessieVersion.NESSIE_VERSION); + pb.environment().put("CONTAINER_NAME", container.getContainerName()); + try { + Process process = pb.inheritIO().start(); + process.waitFor(); + if (process.exitValue() != 0) { + throw new RuntimeException("Failed to import Nessie image"); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("resource") + private void setUpK8sClient() { + LOGGER.info("Setting up Kubernetes client..."); + String kubeConfigYaml = container.getKubeConfigYaml(); + Config config = Config.fromKubeconfig(kubeConfigYaml); + k8sClient = + new KubernetesClientBuilder().withConfig(config).build().adapt(OpenShiftClient.class); + } + + private void installCrds() { + LOGGER.info("Installing Nessie CRDs..."); + // quarkus.operator-sdk.crd.apply is not effective when running integration tests, + // so we need to install the CRDs manually + Path crdDir = Paths.get(System.getProperty("nessie.crds.dir", "build/kubernetes")); + try (Stream walk = Files.walk(crdDir)) { + walk.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".projectnessie.org-v1.yml")) + .forEach( + path -> + k8sClient + .apiextensions() + .v1() + .customResourceDefinitions() + .load(path.toFile()) + .createOr(NonDeletingOperation::update)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void setUpUris() { + if (monitoring) { + if (waitForComponents) { + waitForPrometheusReady(); + } + prometheusUri = + URI.create( + "http://localhost:%d".formatted(container.getMappedPort(PROMETHEUS_NODE_PORT))); + } + if (telemetry) { + if (waitForComponents) { + waitForCollectorReady(); + } + } + if (ingress) { + if (waitForComponents) { + waitForTraefikReady(); + } + nessieUri = + URI.create( + "http://localhost:%d/api/v2".formatted(container.getMappedPort(NESSIE_INGRESS_PORT))); + } else { + nessieUri = + URI.create( + "http://localhost:%d/api/v2".formatted(container.getMappedPort(NESSIE_NODE_PORT))); + } + } + + private void waitForPrometheusReady() { + LOGGER.info("Waiting for Prometheus to be ready..."); + new Kubectl() + .waitUntil( + "pod", + "prometheus", + "Ready", + Duration.ofMinutes(2), + "--selector=app.kubernetes.io/instance=prometheus-kube-prometheus-prometheus"); + } + + private void waitForCollectorReady() { + LOGGER.info("Waiting for OpenTelemetry collector to be ready..."); + new Kubectl() + .waitUntil( + "pod", + "otel-collector", + "Ready", + Duration.ofMinutes(2), + "--selector=app.kubernetes.io/instance=otel-collector"); + } + + private void waitForTraefikReady() { + LOGGER.info("Waiting for Ingress to be ready..."); + new Kubectl() + .waitUntil( + "pod", + "kube-system", + "Ready", + Duration.ofMinutes(2), + "--selector=app.kubernetes.io/instance=traefik-kube-system"); + } + + public class Kubectl { + + public ExecResult exec(String... args) { + String[] cmd = new String[args.length + 1]; + cmd[0] = "kubectl"; + System.arraycopy(args, 0, cmd, 1, args.length); + ExecResult result; + try { + // Run kubectl command in the main container, no need to use a sidecar + result = container.execInContainer(cmd); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + if (result.getExitCode() != 0) { + throw new KubectlExecException(cmd, result); + } + return result; + } + + public void deleteAll(String namespace, Duration timeout) { + exec( + "delete", + "all", + "--all", + "--namespace", + namespace, + "--wait", + "--timeout=%ds".formatted(timeout.getSeconds())); + } + + public void waitUntil( + HasMetadata resource, + String namespace, + String condition, + Duration timeout, + String... args) { + waitUntil( + resource.getKind() + "/" + resource.getMetadata().getName(), + namespace, + condition, + timeout, + args); + } + + public void waitUntil( + String name, String namespace, String condition, Duration timeout, String... args) { + String[] cmd = new String[args.length + 5]; + cmd[0] = "wait"; + cmd[1] = "--for=condition=" + condition; + cmd[2] = name; + cmd[3] = "--timeout=%ds".formatted(timeout.getSeconds()); + cmd[4] = "--namespace=" + namespace; + System.arraycopy(args, 0, cmd, 5, args.length); + await() + .atMost(timeout) + .pollInterval(Duration.ofSeconds(1)) + .untilAsserted( + () -> { + try { + exec(cmd); + } catch (KubectlExecException e) { + if (e.getResult().getStderr().contains("no matching resources found")) { + fail(e.getMessage()); // retry until at least one resource is found + } + throw e; + } + }); + } + + public String logs(Pod resource, boolean previous) { + try { + return logs( + resource.getMetadata().getName(), + resource.getMetadata().getNamespace(), + "--previous=" + previous); + } catch (KubectlExecException e) { + if (e.getMessage().contains("not found")) { + return ""; + } + throw e; + } + } + + public String logs(String name, String namespace, String... args) { + String[] cmd = new String[args.length + 3]; + cmd[0] = "logs"; + cmd[1] = name; + cmd[2] = "--namespace=" + namespace; + System.arraycopy(args, 0, cmd, 3, args.length); + return exec(cmd).getStdout(); + } + } + + public static class KubectlExecException extends RuntimeException { + + private final ExecResult result; + + public KubectlExecException(String[] cmd, ExecResult result) { + super("command failed: %s: %s".formatted(Arrays.toString(cmd), result)); + this.result = result; + } + + public ExecResult getResult() { + return result; + } + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/testinfra/KeycloakContainerLifecycleManager.java b/operator/src/intTest/java/org/projectnessie/operator/testinfra/KeycloakContainerLifecycleManager.java new file mode 100644 index 00000000000..98a1e247dd6 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/KeycloakContainerLifecycleManager.java @@ -0,0 +1,72 @@ +/* + * 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.testinfra; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.Annotated; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.MatchesType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.URI; +import java.util.List; +import org.projectnessie.testing.keycloak.CustomKeycloakContainer; +import org.projectnessie.testing.keycloak.ImmutableKeycloakConfig; +import org.testcontainers.containers.wait.strategy.Wait; + +public class KeycloakContainerLifecycleManager + extends AbstractContainerLifecycleManager { + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface InternalRealmUri {} + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ExternalRealmUri {} + + public static final String CLIENT_ID = "nessie"; + + @Override + @SuppressWarnings("resource") + protected CustomKeycloakContainer createContainer() { + return new CustomKeycloakContainer( + ImmutableKeycloakConfig.builder() + .realmConfigure( + realm -> + realm + .getClients() + .add( + CustomKeycloakContainer.createServiceClient( + CLIENT_ID, List.of("email", "profile")))) + .build()) + .waitingFor(Wait.forLogMessage(".*Running the server in development mode.*", 1)); + } + + @Override + public void inject(TestInjector testInjector) { + // The "keycloak" hostname is not resolvable from within the K3s container, so we need to use + // the in-Docker IP address of the Keycloak container instead. + URI internalRealmUri = + URI.create( + container.getInternalRealmUri().toString().replace("keycloak", inDockerIpAddress)); + URI externalRealmUri = container.getExternalRealmUri(); + testInjector.injectIntoFields( + internalRealmUri, new MatchesType(URI.class).and(new Annotated(InternalRealmUri.class))); + testInjector.injectIntoFields( + externalRealmUri, new MatchesType(URI.class).and(new Annotated(ExternalRealmUri.class))); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/testinfra/MongoContainerLifecycleManager.java b/operator/src/intTest/java/org/projectnessie/operator/testinfra/MongoContainerLifecycleManager.java new file mode 100644 index 00000000000..daa587818f3 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/MongoContainerLifecycleManager.java @@ -0,0 +1,66 @@ +/* + * 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.testinfra; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.Annotated; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.MatchesType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.Duration; +import org.intellij.lang.annotations.Language; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; + +public class MongoContainerLifecycleManager + extends AbstractContainerLifecycleManager> { + + public static final String DATABASE_NAME = "nessie"; + public static final int MONGO_PORT = 27017; + + @Language("JavaScript") + private static final String MONGO_INIT_JS = + """ + db.createUser({user: "nessie", pwd: "nessie", roles: [{role: "readWrite", db: "nessie"}]}); + """; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface MongoConnectionString {} + + @SuppressWarnings("resource") + @Override + protected GenericContainer createContainer() { + return new GenericContainer<>(dockerImage("mongo")) + .withEnv("MONGO_INITDB_DATABASE", DATABASE_NAME) + .withExposedPorts(MONGO_PORT) + .withCopyToContainer( + Transferable.of(MONGO_INIT_JS), "/docker-entrypoint-initdb.d/mongo-init.js") + .withStartupTimeout(Duration.ofMinutes(5)) + .waitingFor(Wait.forLogMessage(".*mongod startup complete.*", 1)); + } + + @Override + public void inject(TestInjector testInjector) { + super.inject(testInjector); + String connectionString = String.format("mongodb://%s:%d", inDockerIpAddress, MONGO_PORT); + testInjector.injectIntoFields( + connectionString, + new MatchesType(String.class).and(new Annotated(MongoConnectionString.class))); + } +} diff --git a/operator/src/intTest/java/org/projectnessie/operator/testinfra/PostgresContainerLifecycleManager.java b/operator/src/intTest/java/org/projectnessie/operator/testinfra/PostgresContainerLifecycleManager.java new file mode 100644 index 00000000000..af70c24a940 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/PostgresContainerLifecycleManager.java @@ -0,0 +1,51 @@ +/* + * 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.testinfra; + +import static org.testcontainers.containers.PostgreSQLContainer.POSTGRESQL_PORT; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.Annotated; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager.TestInjector.MatchesType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.testcontainers.containers.PostgreSQLContainer; + +public class PostgresContainerLifecycleManager + extends AbstractContainerLifecycleManager> { + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + public @interface JdbcUrl {} + + @SuppressWarnings("resource") + @Override + protected PostgreSQLContainer createContainer() { + return new PostgreSQLContainer<>(dockerImage("postgres").asCompatibleSubstituteFor("postgres")) + .withDatabaseName("nessie") + .withUsername("nessie") + .withPassword("nessie"); + } + + @Override + public void inject(TestInjector testInjector) { + super.inject(testInjector); + String jdbcUrl = "jdbc:postgresql://%s:%d/nessie".formatted(inDockerIpAddress, POSTGRESQL_PORT); + testInjector.injectIntoFields( + jdbcUrl, new MatchesType(String.class).and(new Annotated(JdbcUrl.class))); + } +} diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/config-map.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/config-map.yaml new file mode 100644 index 00000000000..9bd81c18c9a --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/config-map.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.version.store.persist.bigtable.app-profile-id=nessie + nessie.version.store.persist.bigtable.emulator-host=placeholder.com + nessie.version.store.persist.bigtable.emulator-port=8086 + nessie.version.store.persist.bigtable.instance-id=test-instance + nessie.version.store.persist.cache-capacity-mb=0 + nessie.version.store.type=BIGTABLE + quarkus.google.cloud.project-id=test-project + quarkus.oidc.tenant-enabled=false + quarkus.otel.sdk.disabled=true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/deployment.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/deployment.yaml new file mode 100644 index 00000000000..5db0a3ee3eb --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/deployment.yaml @@ -0,0 +1,96 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + annotations: + projectnessie.org/config-checksum: sha256:placeholder + spec: + containers: + - name: nessie + # noinspection KubernetesUnknownValues + image: "projectnessie/nessie-test-server:@projectVersion@" + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + - name: nessie-debug + containerPort: 5009 + protocol: TCP + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + env: + - name: GOOGLE_APPLICATION_CREDENTIALS + value: "/bigtable-nessie/sa_credentials.json" + - name: JAVA_DEBUG + value: "true" + - name: JAVA_DEBUG_PORT + value: "*:5009" + - name: JAVA_OPTS_APPEND + value: "-XX:+PrintFlagsFinal" + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + - name: bigtable-creds + mountPath: /bigtable-nessie + livenessProbe: + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + volumes: + - name: nessie-config + configMap: + name: nessie-test + - name: bigtable-creds + secret: + secretName: nessie-db-credentials + items: + - key: sa_json + path: sa_credentials.json + serviceAccountName: default diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/nessie.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/nessie.yaml new file mode 100644 index 00000000000..9b92b0bd544 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/nessie.yaml @@ -0,0 +1,54 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + versionStore: + type: BigTable + cache: + enabled: false + bigTable: + projectId: "test-project" + instanceId: "test-instance" + appProfileId: "nessie" + credentials: + secret: nessie-db-credentials + key: sa_json + service: + type: NodePort + nodePort: 30120 + monitoring: + labels: + foo: bar + interval: 1s + remoteDebug: + enabled: true + port: 5009 + jvmOptions: + - -XX:+PrintFlagsFinal + deployment: + image: + repository: projectnessie/nessie-test-server + tag: @projectVersion@ + pullPolicy: Never + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/secret.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/secret.yaml new file mode 100644 index 00000000000..579a9b35b48 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: nessie-db-credentials +type: Opaque +data: + sa_json: ewogICAgICAidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAogICAgICAicHJvamVjdF9pZCI6ICJ0ZXN0LXByb2plY3QiLAogICAgICAicHJpdmF0ZV9rZXlfaWQiOiAiczNjcjN0IgogICAgfQo= +# { +# "type": "service_account", +# "project_id": "test-project", +# "private_key_id": "s3cr3t" +# } diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service-mgmt.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service-monitor.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service-monitor.yaml new file mode 100644 index 00000000000..8852220e45b --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service-monitor.yaml @@ -0,0 +1,26 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + namespaceSelector: + matchNames: + - @namespace@ + endpoints: + - port: nessie-mgmt + scheme: http + path: /q/metrics + interval: 1s diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service.yaml new file mode 100644 index 00000000000..246f0c11c1e --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/bigtable/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + type: NodePort + ports: + - name: nessie-server + protocol: TCP + nodePort: 30120 + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/config-map.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/config-map.yaml new file mode 100644 index 00000000000..e5e88976843 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/config-map.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.version.store.persist.cache-capacity-mb=0 + quarkus.cassandra.auth.username=cassandra + quarkus.cassandra.contact-points=placeholder:9042 + quarkus.cassandra.keyspace=nessie + quarkus.cassandra.local-datacenter=datacenter1 + nessie.version.store.type=CASSANDRA + quarkus.oidc.tenant-enabled=false + quarkus.otel.exporter.otlp.traces.endpoint=http://otel-collector-opentelemetry-collector.otel-collector.svc.cluster.local:4317 + quarkus.otel.resource.attributes=service.name=nessie-test-custom + quarkus.otel.traces.sampler=parentbased_always_on diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/deployment.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/deployment.yaml new file mode 100644 index 00000000000..41abd99e24a --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + annotations: + projectnessie.org/config-checksum: sha256:placeholder + spec: + containers: + - name: nessie + # noinspection KubernetesUnknownValues + image: "projectnessie/nessie-test-server:@projectVersion@" + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: quarkus.cassandra.auth.password + valueFrom: + secretKeyRef: + name: nessie-db-credentials + key: password + livenessProbe: + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + volumes: + - name: nessie-config + configMap: + name: nessie-test + serviceAccountName: nessie-test diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/init.cql b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/init.cql new file mode 100644 index 00000000000..a8ca0c586a3 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/init.cql @@ -0,0 +1,17 @@ +/* + * 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. + */ + +CREATE KEYSPACE IF NOT EXISTS nessie WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }; diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/nessie.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/nessie.yaml new file mode 100644 index 00000000000..0f62e9a7289 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/nessie.yaml @@ -0,0 +1,57 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + versionStore: + type: Cassandra + cassandra: + contactPoints: + - cassandra.cassandra.svc.cluster.local:9042 + localDatacenter: datacenter1 + keyspace: nessie + username: cassandra + password: + secret: nessie-db-credentials + key: password + service: + type: NodePort + nodePort: 30120 + monitoring: + enabled: false + telemetry: + enabled: true + endpoint: http://otel-collector-opentelemetry-collector.otel-collector.svc.cluster.local:4317 + sample: all + attributes: + service.name: nessie-test-custom + advancedConfig: + nessie.version.store.persist.cache-capacity-mb: "0" + deployment: + image: + repository: projectnessie/nessie-test-server + tag: @projectVersion@ + pullPolicy: Never + serviceAccount: + create: true + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/secret.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/secret.yaml new file mode 100644 index 00000000000..6dfb605fbf3 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: nessie-db-credentials +type: Opaque +data: + password: Y2Fzc2FuZHJh #cassandra diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service-account.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service-account.yaml new file mode 100644 index 00000000000..375880463c7 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service-account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service-mgmt.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service.yaml new file mode 100644 index 00000000000..246f0c11c1e --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/cassandra/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + type: NodePort + ports: + - name: nessie-server + protocol: TCP + nodePort: 30120 + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/config-map.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/config-map.yaml new file mode 100644 index 00000000000..d490dcebd4f --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/config-map.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.version.store.persist.cache-capacity-mb=32 + nessie.version.store.type=DYNAMODB + quarkus.dynamodb.aws.region=us-west-2 + quarkus.dynamodb.endpoint-override=https://placeholder.com + quarkus.oidc.tenant-enabled=false + quarkus.otel.sdk.disabled=true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/deployment.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/deployment.yaml new file mode 100644 index 00000000000..3554b0eb8a2 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/deployment.yaml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + annotations: + projectnessie.org/config-checksum: sha256:placeholder + spec: + containers: + - name: nessie + # noinspection KubernetesUnknownValues + image: "projectnessie/nessie-test-server:@projectVersion@" + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: nessie-dynamo-credentials + key: accessKey + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: nessie-dynamo-credentials + key: secretKey + livenessProbe: + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + # "injected" side-car + initContainers: + - name: sidecar + image: k8s.gcr.io/pause + imagePullPolicy: IfNotPresent + volumes: + - name: nessie-config + configMap: + name: nessie-test + serviceAccountName: nessie-test diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/ingress.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/ingress.yaml new file mode 100644 index 00000000000..43bff4ed7d4 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/ingress.yaml @@ -0,0 +1,26 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + foo: bar +spec: + rules: + - host: nessie.example.com + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: nessie-test + port: + number: 19120 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/nessie.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/nessie.yaml new file mode 100644 index 00000000000..a55fdd639bc --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/nessie.yaml @@ -0,0 +1,55 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + versionStore: + type: DynamoDb + cache: + fixedSize: 32Mi + dynamoDb: + credentials: + secret: nessie-dynamo-credentials + accessKeyId: accessKey + secretAccessKey: secretKey + region: us-west-2 + ingress: + enabled: true + annotations: + foo: bar + rules: + - host: nessie.example.com + paths: + - / + service: + type: LoadBalancer + monitoring: + enabled: false + deployment: + image: + repository: projectnessie/nessie-test-server + tag: @projectVersion@ + pullPolicy: Never + serviceAccount: + create: true + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/secret.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/secret.yaml new file mode 100644 index 00000000000..f74d9099e37 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: nessie-dynamo-credentials +type: Opaque +data: + accessKey: bmVzc2ll #nessie + secretKey: bmVzc2ll #nessie diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service-account.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service-account.yaml new file mode 100644 index 00000000000..375880463c7 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service-account.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service-mgmt.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service.yaml new file mode 100644 index 00000000000..ef33edf21a4 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/dynamo/service.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + type: LoadBalancer + ports: + - name: nessie-server + protocol: TCP + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/autoscaler.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/autoscaler.yaml new file mode 100644 index 00000000000..08df037ddc9 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/autoscaler.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: nessie-test +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nessie-test + minReplicas: 2 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 99 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/config-map.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/config-map.yaml new file mode 100644 index 00000000000..bed8935f724 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/config-map.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.version.store.persist.cache-capacity-fraction-adjust-mb=66 + nessie.version.store.persist.cache-capacity-fraction-min-size-mb=65 + nessie.version.store.persist.cache-capacity-fraction-of-heap=0.6 + nessie.version.store.persist.jdbc.datasource=postgresql + nessie.version.store.type=JDBC + quarkus.datasource.postgresql.jdbc.url=jdbc:postgresql://placeholder.com:5432/ + quarkus.datasource.postgresql.username=nessie + quarkus.oidc.tenant-enabled=false + quarkus.otel.sdk.disabled=true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/deployment.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/deployment.yaml new file mode 100644 index 00000000000..4fb6529d204 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/deployment.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + # replicas not set because of HPA + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + foo: bar + annotations: + projectnessie.org/config-checksum: sha256:placeholder + foo: bar + spec: + containers: + - name: nessie + # noinspection KubernetesUnknownValues + image: "projectnessie/nessie-test-server:@projectVersion@" + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: NESSIE_SERVER_DEFAULT_BRANCH + value: my-branch + - name: quarkus.datasource.postgresql.password + valueFrom: + secretKeyRef: + name: nessie-db-credentials + key: password + livenessProbe: + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + volumes: + - name: nessie-config + configMap: + name: nessie-test + serviceAccountName: nessie-test-custom-service-account diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/nessie.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/nessie.yaml new file mode 100644 index 00000000000..4b63200b6df --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/nessie.yaml @@ -0,0 +1,71 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + versionStore: + type: Jdbc + cache: + enabled: true + heapFraction: 600m + minSize: 65Mi + minFreeHeap: 66Mi + jdbc: + url: jdbc:postgresql://postgresql.postgresql.svc.cluster.local:5432/nessie + username: nessie + password: + secret: nessie-db-credentials + key: password + extraEnv: + - name: NESSIE_SERVER_DEFAULT_BRANCH + value: my-branch + service: + type: NodePort + nodePort: 30120 + sessionAffinity: ClientIP + labels: + foo: bar + annotations: + foo: bar + monitoring: + enabled: false + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 2 + targetCpuUtilizationPercentage: 99 + deployment: + image: + repository: projectnessie/nessie-test-server + tag: @projectVersion@ + pullPolicy: Never + labels: + foo: bar + annotations: + foo: bar + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + serviceAccount: + create: true + name: nessie-test-custom-service-account + annotations: + foo: bar + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/secret.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/secret.yaml new file mode 100644 index 00000000000..cdda8ac4791 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: nessie-db-credentials +type: Opaque +data: + password: bmVzc2ll #nessie diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service-account.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service-account.yaml new file mode 100644 index 00000000000..0818154f142 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service-account.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nessie-test-custom-service-account + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + foo: bar diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service-mgmt.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service-mgmt.yaml new file mode 100644 index 00000000000..62c96fd5125 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service-mgmt.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar + annotations: + foo: bar +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service.yaml new file mode 100644 index 00000000000..3b4a714166a --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/jdbc/service.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar + annotations: + foo: bar +spec: + type: NodePort + ports: + - name: nessie-server + protocol: TCP + nodePort: 30120 + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + sessionAffinity: ClientIP diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/config-map.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/config-map.yaml new file mode 100644 index 00000000000..39444d47fdd --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/config-map.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.version.store.persist.cache-capacity-mb=0 + nessie.version.store.type=MONGODB + quarkus.mongodb.connection-string=mongodb://placeholder.com:27017/nessie?ssl=false + quarkus.mongodb.credentials.username=nessie + quarkus.mongodb.database=nessie + quarkus.oidc.tenant-enabled=false + quarkus.otel.sdk.disabled=true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/deployment.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/deployment.yaml new file mode 100644 index 00000000000..baa69bdc81c --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/deployment.yaml @@ -0,0 +1,84 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + annotations: + projectnessie.org/config-checksum: sha256:placeholder + spec: + containers: + - name: nessie + # noinspection KubernetesUnknownValues + image: "projectnessie/nessie-test-server:@projectVersion@" + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: quarkus.mongodb.credentials.password + valueFrom: + secretKeyRef: + name: nessie-db-credentials + key: password + livenessProbe: + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + volumes: + - name: nessie-config + configMap: + name: nessie-test + serviceAccountName: nessie-test-custom-service-account diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/nessie.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/nessie.yaml new file mode 100644 index 00000000000..724b17f2ec3 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/nessie.yaml @@ -0,0 +1,49 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + versionStore: + type: MongoDb + cache: + enabled: false + mongoDb: + connectionString: mongodb://mongodb.mongodb.svc.cluster.local:27017/nessie?ssl=false + database: nessie + username: nessie + password: + secret: nessie-db-credentials + key: password + service: + type: NodePort + nodePort: 30120 + monitoring: + enabled: false + deployment: + image: + repository: projectnessie/nessie-test-server + tag: @projectVersion@ + pullPolicy: Never + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + serviceAccount: + name: nessie-test-custom-service-account + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/secret.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/secret.yaml new file mode 100644 index 00000000000..cdda8ac4791 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: nessie-db-credentials +type: Opaque +data: + password: bmVzc2ll #nessie diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service-account.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service-account.yaml new file mode 100644 index 00000000000..6fa902ae4db --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service-account.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nessie-test-custom-service-account diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service-mgmt.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service.yaml new file mode 100644 index 00000000000..246f0c11c1e --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/mongo/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + type: NodePort + ports: + - name: nessie-server + protocol: TCP + nodePort: 30120 + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/config-map.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/config-map.yaml new file mode 100644 index 00000000000..6abfbfaeb4e --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/config-map.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.server.authentication.anonymous-paths=/q/health/live,/q/health/live/,/q/health/ready,/q/health/ready/,/q/metrics,/q/metrics/ + nessie.server.authentication.enabled=true + nessie.server.authorization.enabled=true + nessie.server.authorization.rules.allow_branch_listing=op=='VIEW_REFERENCE' && role.startsWith('service-account-nessie') && ref.startsWith('main') + nessie.server.authorization.rules.allow_commits=op=='COMMIT_CHANGE_AGAINST_REFERENCE' && role.startsWith('service-account-nessie') && ref.startsWith('main') + nessie.server.authorization.rules.allow_create_entities=op=='CREATE_ENTITY' && role.startsWith('service-account-nessie') && ref.startsWith('main') + nessie.version.store.persist.cache-capacity-mb=0 + nessie.version.store.persist.repository-id=my-repository + nessie.version.store.persist.rocks.database-path=/rocks-nessie + nessie.version.store.type=ROCKSDB + quarkus.oidc.auth-server-url=https://placeholder.com + quarkus.oidc.client-id=nessie + quarkus.otel.sdk.disabled=true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/deployment.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/deployment.yaml new file mode 100644 index 00000000000..4590c6b03e8 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/deployment.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + foo: bar + annotations: + projectnessie.org/config-checksum: sha256:placeholder + foo: bar + spec: + containers: + - name: nessie + # noinspection KubernetesUnknownValues + image: "projectnessie/nessie-test-server:@projectVersion@" + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + - mountPath: /rocks-nessie + name: rocks-storage + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: QUARKUS_OIDC_TOKEN_ISSUER + value: any + livenessProbe: + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + volumes: + - name: nessie-config + configMap: + name: nessie-test + - name: rocks-storage + persistentVolumeClaim: + claimName: nessie-test + serviceAccountName: nessie-test-sa diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/nessie.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/nessie.yaml new file mode 100644 index 00000000000..27074d4f2e5 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/nessie.yaml @@ -0,0 +1,69 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + size: 1 + versionStore: + type: RocksDb + rocksDb: + storageSize: 64m + authentication: + enabled: true + oidcAuthServerUrl: https://example.com + oidcClientId: nessie + authorization: + enabled: true + rules: + # role name comes form JWT token cf. "preferred_username" field + allow_branch_listing: op=='VIEW_REFERENCE' && role.startsWith('service-account-nessie') && ref.startsWith('main') + allow_commits: op=='COMMIT_CHANGE_AGAINST_REFERENCE' && role.startsWith('service-account-nessie') && ref.startsWith('main') + allow_create_entities: op=='CREATE_ENTITY' && role.startsWith('service-account-nessie') && ref.startsWith('main') + extraEnv: + - name: QUARKUS_OIDC_TOKEN_ISSUER + value: any + advancedConfig: + nessie.version.store.persist: + repository-id: my-repository + cache-capacity-mb: "0" + nessie.server.authentication.anonymous-paths: /q/health/live,/q/health/live/,/q/health/ready,/q/health/ready/,/q/metrics,/q/metrics/ + service: + type: NodePort + nodePort: 30120 + monitoring: + enabled: false + deployment: + image: + repository: projectnessie/nessie-test-server + tag: @projectVersion@ + pullPolicy: Never + labels: + foo: bar + annotations: + foo: bar + resources: + requests: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + limits: + cpu: 800m + memory: 384Mi + ephemeral-storage: 10Mi + serviceAccount: + create: true + name: nessie-test-sa + annotations: + foo: bar + livenessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 + readinessProbe: + initialDelaySeconds: 5 + periodSeconds: 3 + timeoutSeconds: 10 + successThreshold: 1 + failureThreshold: 20 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/pvc.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/pvc.yaml new file mode 100644 index 00000000000..b92ed22bbb6 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/pvc.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 64m +# storageClassName: standard + volumeName: pvc-600ce745-6f74-4048-84af-0d9d18263e0e diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service-account.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service-account.yaml new file mode 100644 index 00000000000..c78ae770838 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service-account.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nessie-test-sa + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + foo: bar diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service-mgmt.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service.yaml b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service.yaml new file mode 100644 index 00000000000..246f0c11c1e --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/inttests/fixtures/rocks/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + type: NodePort + ports: + - name: nessie-server + protocol: TCP + nodePort: 30120 + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test diff --git a/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-bigtable-version b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-bigtable-version new file mode 100644 index 00000000000..3b48633817f --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-bigtable-version @@ -0,0 +1,5 @@ +# Dockerfile to provide the image name and tag to a test. +# Version is managed by Renovate - do not edit. +# See https://cloud.google.com/sdk/docs/downloads-docker#docker_image_options +# Use debian_component_based because it supports linux/arm +FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:478.0.0-debian_component_based@sha256:9c1f1cc070e4c58ed094cbb7ccb6176341b6587f17ccb91359996d81333bd082 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-cassandra-version b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-cassandra-version new file mode 100644 index 00000000000..442e5f3b691 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-cassandra-version @@ -0,0 +1,3 @@ +# Dockerfile to provide the image name and tag to a test. +# Version is managed by Renovate - do not edit. +FROM docker.io/cassandra:5.0@sha256:3869be6ad2d0819542ccb4e1ebd4c2239e5fd756eaea821c5c81c02487a2c365 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-dynamo-version b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-dynamo-version new file mode 100644 index 00000000000..c3d183e6ea5 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-dynamo-version @@ -0,0 +1,3 @@ +# Dockerfile to provide the image name and tag to a test. +# Version is managed by Renovate - do not edit. +FROM docker.io/amazon/dynamodb-local:2.5.0@sha256:22b1f856581e5c77549b319c20371c7454381d7ef039d2a386c1b0dc686573cd diff --git a/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-k3s-version b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-k3s-version new file mode 100644 index 00000000000..29575c02906 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-k3s-version @@ -0,0 +1,5 @@ +# Dockerfile to provide the image name and tag to a test. +# Version is managed by Renovate - do not edit. +FROM rancher/k3s:v1.30.1-k3s1@sha256:09e019280cdc89d038644f1656ac7f2aed98807bd97c20e2dc1b5b9f534a0718 + + diff --git a/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-mongo-version b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-mongo-version new file mode 100644 index 00000000000..a5cf23b80fb --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-mongo-version @@ -0,0 +1,3 @@ +# Dockerfile to provide the image name and tag to a test. +# Version is managed by Renovate - do not edit. +FROM mongo:7.0.11@sha256:8f9f843d383e358d9be2f172ba9d2455e8736f3c59b00330da1f1b44273ce267 diff --git a/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-postgres-version b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-postgres-version new file mode 100644 index 00000000000..15234854dee --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-postgres-version @@ -0,0 +1,3 @@ +# Dockerfile to provide the image name and tag to a test. +# Version is managed by Renovate - do not edit. +FROM postgres:16.3@sha256:1bf73ccae25238fa555100080042f0b2f9be08eb757e200fe6afc1fc413a1b3c diff --git a/operator/src/main/java/org/projectnessie/operator/events/EventReason.java b/operator/src/main/java/org/projectnessie/operator/events/EventReason.java new file mode 100644 index 00000000000..e9f88174747 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/events/EventReason.java @@ -0,0 +1,57 @@ +/* + * 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.events; + +public enum EventReason { + + // Normal events + CreatingServiceAccount(EventType.Normal), + CreatingConfigMap(EventType.Normal), + CreatingPersistentVolumeClaim(EventType.Normal), + CreatingDeployment(EventType.Normal), + CreatingService(EventType.Normal), + CreatingMgmtService(EventType.Normal), + CreatingServiceMonitor(EventType.Normal), + CreatingIngress(EventType.Normal), + CreatingHPA(EventType.Normal), + ReconcileSuccess(EventType.Normal), + + // Warning events + InvalidName(EventType.Warning), + InvalidAuthenticationConfig(EventType.Warning), + InvalidAuthorizationConfig(EventType.Warning), + InvalidTelemetryConfig(EventType.Warning), + InvalidAutoScalingConfig(EventType.Warning), + InvalidIngressConfig(EventType.Warning), + InvalidVersionStoreConfig(EventType.Warning), + InvalidAdvancedConfig(EventType.Warning), + DuplicateEnvVar(EventType.Warning), + MultipleReplicasNotAllowed(EventType.Warning), + AutoscalingNotAllowed(EventType.Warning), + ServiceMonitorNotSupported(EventType.Warning), + ReconcileError(EventType.Warning), + ; + + private final EventType type; + + EventReason(EventType type) { + this.type = type; + } + + public EventType type() { + return type; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/events/EventService.java b/operator/src/main/java/org/projectnessie/operator/events/EventService.java new file mode 100644 index 00000000000..9901549649d --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/events/EventService.java @@ -0,0 +1,245 @@ +/* + * 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.events; + +import io.fabric8.kubernetes.api.model.Event; +import io.fabric8.kubernetes.api.model.EventBuilder; +import io.fabric8.kubernetes.api.model.EventList; +import io.fabric8.kubernetes.api.model.EventSource; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.MicroTime; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.ObjectReferenceBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.javaoperatorsdk.operator.AggregatedOperatorException; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.net.HttpURLConnection; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +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.utils.EventUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Service to manage events. + * + *

Events are unique for each combination of primary resource and reason. The event is updated + * when an event with the same reason is fired again for the same resource. + * + *

Loosely inspired from event_broadcaster.go. + */ +@ApplicationScoped +public class EventService { + + private static final String CONTEXT_KEY = "event-service"; + + public static EventService retrieveFromContext(Context context) { + return context.managedDependentResourceContext().getMandatory(CONTEXT_KEY, EventService.class); + } + + public static void storeInContext(Context context, EventService eventService) { + context.managedDependentResourceContext().put(CONTEXT_KEY, eventService); + } + + private static final Logger LOGGER = LoggerFactory.getLogger(EventService.class); + + private final ConcurrentMap> eventsCache = + new ConcurrentHashMap<>(); + + private final KubernetesClient client; + + @Inject + public EventService(KubernetesClient client) { + this.client = client; + } + + public void fireEvent(HasMetadata primary, EventReason reason, String message, Object... args) { + eventsCache + .computeIfAbsent(primary.getMetadata().getUid(), uid -> loadEvents(primary)) + .compute(reason, (r, ev) -> createOrUpdateEvent(1, primary, r, ev, message, args)); + } + + public void fireErrorEvent(HasMetadata primary, Throwable t) { + t = EventUtils.launderThrowable(t, AggregatedOperatorException.class); + if (t instanceof AggregatedOperatorException aoe) { + aoe.getAggregatedExceptions().values().stream() + .map(e -> EventUtils.launderThrowable(e, NessieOperatorException.class)) + .forEach( + error -> + fireEvent( + primary, EventUtils.errorReason(error), EventUtils.getErrorMessage(error))); + } else { + t = EventUtils.launderThrowable(t, NessieOperatorException.class); + fireEvent(primary, EventUtils.errorReason(t), EventUtils.getErrorMessage(t)); + } + } + + private Event createOrUpdateEvent( + int attempt, + HasMetadata primary, + EventReason reason, + Event current, + String message, + Object... args) { + try { + ZonedDateTime now = ZonedDateTime.now(); + String timestamp = EventUtils.formatTime(now); + MicroTime microTime = new MicroTime(EventUtils.formatMicroTime(now)); + String formatted = EventUtils.formatMessage(message, args); + Event updated = + current == null + ? newEvent(primary, reason, formatted, timestamp, microTime) + : editEvent(current, formatted, timestamp, microTime); + Resource resource = client.v1().events().resource(updated); + // Note: server-side apply would be a good option, but it's not compatible with unit tests + return current == null ? resource.create() : resource.update(); + } catch (Exception e) { + // We are the only ones updating these events, but conflicts can happen when + // bouncing the operator pod or reinstalling the operator, since there could + // be more than one operator instance alive for a short period of time. + if (e instanceof KubernetesClientException kce + && kce.getCode() == HttpURLConnection.HTTP_CONFLICT + && attempt < 3) { + LOGGER.debug("Event was updated concurrently, retrying"); + current = client.v1().events().resource(current).require(); + return createOrUpdateEvent(attempt + 1, primary, reason, current, message, args); + } + LOGGER.warn("Failed to create or update event", e); + return current; + } + } + + private ConcurrentMap loadEvents(HasMetadata primary) { + ConcurrentMap events = new ConcurrentHashMap<>(); + try { + for (Event event : eventsFor(primary).list().getItems()) { + EventReason reason = EventUtils.reasonFromEventName(event.getMetadata().getName()); + events.put(reason, event); + } + } catch (Exception e) { + LOGGER.warn("Failed to load events", e); + } + if (!events.isEmpty()) { + LOGGER.info("Loaded {} events", events.size()); + } + return events; + } + + public void clearEvents(HasMetadata primary) { + LOGGER.debug("Deleting events"); + eventsCache.remove(primary.getMetadata().getUid()); + try { + eventsFor(primary).delete(); + } catch (Exception e) { + LOGGER.warn("Failed to delete events", e); + } + } + + private FilterWatchListDeletable> eventsFor( + HasMetadata primary) { + return client + .v1() + .events() + .inNamespace(primary.getMetadata().getNamespace()) + .withInvolvedObject( + new ObjectReferenceBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .withUid(primary.getMetadata().getUid()) + .build()); + } + + private Event newEvent( + HasMetadata primary, + EventReason reason, + String formatted, + String timestamp, + MicroTime microTime) { + String eventName = EventUtils.eventName(primary, reason); + LOGGER.debug("Creating event {}", eventName); + return new EventBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withName(eventName) + .withNamespace(primary.getMetadata().getNamespace()) + .build()) + .withEventTime(microTime) + .withType(reason.type().name()) + .withReason(reason.name()) + .withMessage(formatted) + .withAction("Reconcile") + .withCount(1) + .withFirstTimestamp(timestamp) + .withLastTimestamp(timestamp) + .withInvolvedObject( + new ObjectReferenceBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .withUid(primary.getMetadata().getUid()) + .withResourceVersion(primary.getMetadata().getResourceVersion()) + .withApiVersion(primary.getApiVersion()) + .withKind(primary.getKind()) + .build()) + .withSource(new EventSource(getComponent(primary), null)) + .withReportingComponent(getComponent(primary)) + // TODO add complete pod name + .withReportingInstance("nessie-operator") + .build(); + } + + private Event editEvent(Event current, String formatted, String timestamp, MicroTime microTime) { + EventBuilder eventBuilder = + new EventBuilder(current) + .editMetadata() + .withManagedFields(Collections.emptyList()) + .endMetadata() + .withMessage(formatted) + .withLastTimestamp(timestamp) + .editOrNewSeries() + .withLastObservedTime(microTime) + .endSeries(); + // Only update the count if the message has changed, otherwise + // updating the last observed time only is enough + if (!formatted.equals(current.getMessage())) { + int count = current.getCount() == null ? 1 : current.getCount(); + count++; + eventBuilder.withCount(count).editOrNewSeries().withCount(count).endSeries(); + } + Event event = eventBuilder.build(); + LOGGER.debug( + "Updating event {}, new count = {}", current.getMetadata().getName(), event.getCount()); + return event; + } + + private static String getComponent(HasMetadata primary) { + return switch (primary.getKind()) { + case Nessie.KIND -> NessieReconciler.NAME; + default -> throw new IllegalArgumentException("Unknown kind " + primary.getKind()); + }; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/events/EventType.java b/operator/src/main/java/org/projectnessie/operator/events/EventType.java new file mode 100644 index 00000000000..b2d7dc7c6c5 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/events/EventType.java @@ -0,0 +1,21 @@ +/* + * 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.events; + +public enum EventType { + Normal, + Warning +} diff --git a/operator/src/main/java/org/projectnessie/operator/exception/InvalidSpecException.java b/operator/src/main/java/org/projectnessie/operator/exception/InvalidSpecException.java new file mode 100644 index 00000000000..0e6a316c871 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/exception/InvalidSpecException.java @@ -0,0 +1,32 @@ +/* + * 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.exception; + +import org.projectnessie.operator.events.EventReason; + +public class InvalidSpecException extends NessieOperatorException { + + private final EventReason reason; + + public InvalidSpecException(EventReason reason, String message) { + super(message); + this.reason = reason; + } + + public EventReason getReason() { + return reason; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/exception/NessieOperatorException.java b/operator/src/main/java/org/projectnessie/operator/exception/NessieOperatorException.java new file mode 100644 index 00000000000..3c0f2477839 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/exception/NessieOperatorException.java @@ -0,0 +1,23 @@ +/* + * 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.exception; + +public class NessieOperatorException extends RuntimeException { + + public NessieOperatorException(String message) { + super(message); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/AbstractReconciler.java b/operator/src/main/java/org/projectnessie/operator/reconciler/AbstractReconciler.java new file mode 100644 index 00000000000..f1c8f4d57aa --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/AbstractReconciler.java @@ -0,0 +1,112 @@ +/* + * 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; + +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult; +import io.quarkiverse.operatorsdk.annotations.CSVMetadata; +import io.quarkiverse.operatorsdk.annotations.CSVMetadata.Icon; +import io.quarkiverse.operatorsdk.annotations.CSVMetadata.Provider; +import io.quarkiverse.operatorsdk.annotations.SharedCSVMetadata; +import jakarta.inject.Inject; +import org.projectnessie.operator.events.EventService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@CSVMetadata( + bundleName = "nessie-operator", + icon = @Icon(fileName = "nessie.svg"), + provider = @Provider(name = "Project Nessie", url = "https://projectnessie.org")) +public abstract class AbstractReconciler + implements Reconciler, + ContextInitializer, + Cleaner, + ErrorStatusHandler, + SharedCSVMetadata { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractReconciler.class); + + @Inject protected KubernetesHelper kubernetesHelper; + @Inject protected EventService eventService; + + @Override + public void initContext(T primary, Context context) { + LOGGER.debug("Starting reconciliation"); + if (!primary.isMarkedForDeletion()) { + validate(primary); + } + KubernetesHelper.storeInContext(context, kubernetesHelper); + EventService.storeInContext(context, eventService); + } + + @Override + public UpdateControl reconcile(T primary, Context context) { + boolean ready = + context + .managedDependentResourceContext() + .getWorkflowReconcileResult() + .map(wrr -> checkDependentsReady(primary, wrr)) + .orElse(false); + LOGGER.debug("Dependents ready? {}", ready); + if (ready && !isReady(primary)) { + eventService.fireEvent(primary, ReconcileSuccess, "Reconciled successfully"); + } + updatePrimaryStatus(primary, context, ready); + // Note: patch may accidentally result in duplicate elements in collections, esp. conditions + return UpdateControl.updateStatus(primary); + } + + @Override + public ErrorStatusUpdateControl updateErrorStatus( + T primary, Context context, Exception error) { + LOGGER.error("Reconcile failed unexpectedly", error); + eventService.fireErrorEvent(primary, error); + updatePrimaryStatus(primary, context, false); + return ErrorStatusUpdateControl.updateStatus(primary); + } + + @Override + public DeleteControl cleanup(T primary, Context context) { + LOGGER.debug("Resource deleted"); + eventService.clearEvents(primary); + return DeleteControl.defaultDelete(); + } + + protected boolean checkDependentsReady(T primary, WorkflowReconcileResult wrr) { + if (wrr.erroredDependentsExist()) { + wrr.getErroredDependents() + .values() + .forEach(error -> eventService.fireErrorEvent(primary, error)); + } + return wrr.allDependentResourcesReady(); + } + + protected abstract void validate(T primary); + + protected abstract boolean isReady(T primary); + + protected abstract void updatePrimaryStatus(T nessie, Context context, boolean ready); +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/KubernetesHelper.java b/operator/src/main/java/org/projectnessie/operator/reconciler/KubernetesHelper.java new file mode 100644 index 00000000000..2b3f029368c --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/KubernetesHelper.java @@ -0,0 +1,170 @@ +/* + * 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; + +import io.fabric8.kubernetes.api.model.APIGroup; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.VersionInfo; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.quarkus.runtime.Startup; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import java.util.Map; +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.utils.ResourceUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Dependent +public final class KubernetesHelper { + + private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesHelper.class); + + private static final String HELPER_CONTEXT_KEY = "kube-helper"; + + public static KubernetesHelper retrieveFromContext(Context context) { + return context + .managedDependentResourceContext() + .getMandatory(HELPER_CONTEXT_KEY, KubernetesHelper.class); + } + + public static void storeInContext(Context context, KubernetesHelper kubernetesHelper) { + context.managedDependentResourceContext().put(HELPER_CONTEXT_KEY, kubernetesHelper); + } + + private final KubernetesClient client; + private final String operatorVersion; + + @Inject + public KubernetesHelper( + @SuppressWarnings("CdiInjectionPointsInspection") KubernetesClient client, + @ConfigProperty(name = "quarkus.application.version") String operatorVersion) { + this.client = client; + this.operatorVersion = operatorVersion; + } + + @Startup + public void logStartupInfo() { + LOGGER.info("Nessie operator version: {}", getOperatorVersion()); + LOGGER.info( + "Kubernetes cluster version: {}.{}", + getKubernetesVersion().getMajor(), + getKubernetesVersion().getMinor()); + } + + public VersionInfo getKubernetesVersion() { + return client.getKubernetesVersion(); + } + + public String getOperatorVersion() { + return operatorVersion; + } + + /** + * Create metadata for a dependent resource. The dependent resource name will be identical to the + * primary resource name. + */ + public ObjectMetaBuilder metaBuilder(HasMetadata primary) { + return metaBuilder(primary, primary.getMetadata().getName()); + } + + /** + * Create metadata for a dependent resource with the given name and all recommended meta labels. + * + * @see Recommended + * Labels + */ + public ObjectMetaBuilder metaBuilder(HasMetadata primary, String name) { + ResourceUtils.validateName(name); + return new ObjectMetaBuilder() + .withName(name) + .withNamespace(primary.getMetadata().getNamespace()) + .withLabels(selectorLabels(primary)) + .addToLabels( + Map.of( + "app.kubernetes.io/version", + operatorVersion, + "app.kubernetes.io/component", + "nessie", + "app.kubernetes.io/part-of", + "nessie", + "app.kubernetes.io/managed-by", + managedBy(primary))); + } + + /** + * Defines the value of the "app.kubernetes.io/managed-by" label. This label is special because it + * is used as a label selector to watch secondary dependent resources. + */ + public static String managedBy(HasMetadata primary) { + return switch (primary.getKind()) { + case Nessie.KIND -> NessieReconciler.NAME; + default -> + throw new IllegalArgumentException("Unsupported primary resource: " + primary.getKind()); + }; + } + + /** + * Create selector labels for the given primary resource. These labels are suitable for use when + * selecting pods belonging to this primary, e.g. in deployments, services and service monitors. + */ + public Map selectorLabels(HasMetadata primary) { + return Map.of( + "app.kubernetes.io/name", + primary.getSingular(), + "app.kubernetes.io/instance", + primary.getMetadata().getName()); + } + + public boolean isApiSupported(String apiGroup, String apiVersion) { + APIGroup group = client.getApiGroup(apiGroup); + boolean supported = false; + if (group != null) { + supported = group.getVersions().stream().anyMatch(v -> v.getVersion().equals(apiVersion)); + } + LOGGER.debug("API {}/{} supported: {}", apiGroup, apiVersion, supported); + return supported; + } + + public boolean isMonitoringSupported() { + return isApiSupported("monitoring.coreos.com", "v1"); + } + + public boolean isIngressV1Supported() { + return isApiSupported("networking.k8s.io", "v1"); + } + + public boolean isIngressV1Beta1Supported() { + return !isIngressV1Supported() && isApiSupported("networking.k8s.io", "v1beta1"); + } + + public boolean isAutoscalingV2Supported() { + return isApiSupported("autoscaling", "v2"); + } + + public boolean isAutoscalingV2Beta2Supported() { + return !isAutoscalingV2Supported() && isApiSupported("autoscaling", "v2beta2"); + } + + public boolean isAutoscalingV2Beta1Supported() { + return !isAutoscalingV2Beta2Supported() && isApiSupported("autoscaling", "v2beta1"); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/NessieReconciler.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/NessieReconciler.java new file mode 100644 index 00000000000..2ec4140e3f3 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/NessieReconciler.java @@ -0,0 +1,162 @@ +/* + * 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; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_ALL_NAMESPACES; +import static org.projectnessie.operator.reconciler.nessie.NessieReconciler.NESSIE_SERVICES_EVENT_SOURCE; + +import io.fabric8.kubernetes.api.model.Service; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.quarkiverse.operatorsdk.annotations.RBACRule; +import java.util.Map; +import org.projectnessie.operator.reconciler.AbstractReconciler; +import org.projectnessie.operator.reconciler.nessie.dependent.ConfigMapDependent; +import org.projectnessie.operator.reconciler.nessie.dependent.DeploymentDependent; +import org.projectnessie.operator.reconciler.nessie.dependent.HorizontalPodAutoscalerV2Beta1Dependent; +import org.projectnessie.operator.reconciler.nessie.dependent.HorizontalPodAutoscalerV2Beta2Dependent; +import org.projectnessie.operator.reconciler.nessie.dependent.HorizontalPodAutoscalerV2Dependent; +import org.projectnessie.operator.reconciler.nessie.dependent.IngressV1Beta1Dependent; +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.PersistentVolumeClaimDependent; +import org.projectnessie.operator.reconciler.nessie.dependent.ServiceAccountDependent; +import org.projectnessie.operator.reconciler.nessie.dependent.ServiceMonitorDependent; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.reconciler.nessie.resource.NessieStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ControllerConfiguration( + name = NessieReconciler.NAME, + namespaces = WATCH_ALL_NAMESPACES, + dependents = { + @Dependent( + name = "service-account", + type = ServiceAccountDependent.class, + activationCondition = ServiceAccountDependent.ActivationCondition.class), + @Dependent(name = "config-map", type = ConfigMapDependent.class), + @Dependent( + name = "pvc", + type = PersistentVolumeClaimDependent.class, + activationCondition = PersistentVolumeClaimDependent.ActivationCondition.class, + readyPostcondition = PersistentVolumeClaimDependent.ReadyCondition.class), + @Dependent( + name = "deployment", + type = DeploymentDependent.class, + dependsOn = "config-map", + readyPostcondition = DeploymentDependent.ReadyCondition.class), + @Dependent( + name = "service", + type = MainServiceDependent.class, + useEventSourceWithName = NESSIE_SERVICES_EVENT_SOURCE, + dependsOn = "deployment"), + @Dependent( + name = "service-mgmt", + type = ManagementServiceDependent.class, + useEventSourceWithName = NESSIE_SERVICES_EVENT_SOURCE, + dependsOn = "deployment"), + @Dependent( + name = "ingress-v1", + type = IngressV1Dependent.class, + dependsOn = "service", + activationCondition = IngressV1Dependent.ActivationCondition.class, + readyPostcondition = IngressV1Dependent.ReadyCondition.class), + @Dependent( + name = "ingress-v1beta1", + type = IngressV1Beta1Dependent.class, + dependsOn = "service", + activationCondition = IngressV1Beta1Dependent.ActivationCondition.class, + readyPostcondition = IngressV1Beta1Dependent.ReadyCondition.class), + @Dependent( + name = "autoscaler-v2", + type = HorizontalPodAutoscalerV2Dependent.class, + dependsOn = "deployment", + activationCondition = HorizontalPodAutoscalerV2Dependent.ActivationCondition.class), + @Dependent( + name = "autoscaler-v2beta2", + type = HorizontalPodAutoscalerV2Beta2Dependent.class, + dependsOn = "deployment", + activationCondition = HorizontalPodAutoscalerV2Beta2Dependent.ActivationCondition.class), + @Dependent( + name = "autoscaler-v2beta1", + type = HorizontalPodAutoscalerV2Beta1Dependent.class, + dependsOn = "deployment", + activationCondition = HorizontalPodAutoscalerV2Beta1Dependent.ActivationCondition.class), + @Dependent( + name = "service-monitor", + type = ServiceMonitorDependent.class, + dependsOn = "service-mgmt", + activationCondition = ServiceMonitorDependent.ActivationCondition.class), + }) +@RBACRule(apiGroups = "", resources = "events", verbs = RBACRule.ALL) +public class NessieReconciler extends AbstractReconciler + implements EventSourceInitializer { + + public static final String NAME = "nessie-controller"; + + public static final String DEPENDENT_RESOURCES_SELECTOR = "app.kubernetes.io/managed-by=" + NAME; + + public static final String NESSIE_SERVICES_EVENT_SOURCE = "NessieServicesEventSource"; + + private static final Logger LOGGER = LoggerFactory.getLogger(NessieReconciler.class); + + @Override + public Map prepareEventSources(EventSourceContext context) { + InformerEventSource ies = + new InformerEventSource<>( + InformerConfiguration.from(Service.class, context).build(), context); + return Map.of(NESSIE_SERVICES_EVENT_SOURCE, ies); + } + + @Override + protected void validate(Nessie nessie) { + nessie.validate(); + } + + @Override + protected boolean isReady(Nessie primary) { + return primary.getStatus() != null && primary.getStatus().isReady(); + } + + @Override + protected void updatePrimaryStatus(Nessie nessie, Context context, boolean ready) { + if (nessie.getStatus() == null) { + nessie.setStatus(new NessieStatus()); + } + nessie.getStatus().setReady(ready); + if (ready && nessie.getSpec().ingress().enabled()) { + try { + if (kubernetesHelper.isIngressV1Supported()) { + IngressV1Dependent.updateStatus(nessie, context); + } else if (kubernetesHelper.isIngressV1Beta1Supported()) { + IngressV1Beta1Dependent.updateStatus(nessie, context); + } + } catch (Exception e) { + // Can happen if ingress is misconfigured + LOGGER.warn("Failed to compute Ingress URL", e); + nessie.getStatus().setExposedUrl(null); + } + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractHorizontalPodAutoscalerDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractHorizontalPodAutoscalerDependent.java new file mode 100644 index 00000000000..e79bc6a394b --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractHorizontalPodAutoscalerDependent.java @@ -0,0 +1,71 @@ +/* + * 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.CreatingHPA; + +import io.fabric8.kubernetes.api.model.HasMetadata; +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.workflow.Condition; +import org.projectnessie.operator.events.EventService; +import org.projectnessie.operator.reconciler.KubernetesHelper; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AbstractHorizontalPodAutoscalerDependent + extends CRUDKubernetesDependentResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(IngressV1Dependent.class); + + protected AbstractHorizontalPodAutoscalerDependent(Class resourceClass) { + super(resourceClass); + } + + @Override + public HPA create(HPA desired, Nessie nessie, Context context) { + LOGGER.debug( + "Creating horizontal pod autoscaler {} for {}", + desired.getMetadata().getName(), + nessie.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + nessie, + CreatingHPA, + "Creating horizontal pod autoscaler %s", + desired.getMetadata().getName()); + return super.create(desired, nessie, context); + } + + public abstract static class ActivationCondition + implements Condition { + + @Override + public boolean isMet( + DependentResource dependentResource, Nessie nessie, Context context) { + if (nessie.getSpec().autoscaling().enabled() + && nessie.getSpec().versionStore().type().supportsMultipleReplicas()) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + return isAutoscalingSupported(helper); + } + return false; + } + + protected abstract boolean isAutoscalingSupported(KubernetesHelper helper); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractIngressDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractIngressDependent.java new file mode 100644 index 00000000000..d0eaa384e71 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractIngressDependent.java @@ -0,0 +1,96 @@ +/* + * 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 io.fabric8.kubernetes.api.model.HasMetadata.getVersion; +import static org.projectnessie.operator.events.EventReason.CreatingIngress; + +import io.fabric8.kubernetes.api.model.HasMetadata; +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.workflow.Condition; +import org.projectnessie.operator.events.EventService; +import org.projectnessie.operator.reconciler.KubernetesHelper; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractIngressDependent + extends CRUDKubernetesDependentResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractIngressDependent.class); + + protected AbstractIngressDependent(Class resourceClass) { + super(resourceClass); + } + + @Override + public I create(I desired, Nessie nessie, Context context) { + LOGGER.debug( + "Creating ingress {} {} for {}", + getVersion(resourceType()), + desired.getMetadata().getName(), + nessie.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + nessie, CreatingIngress, "Creating ingress %s", desired.getMetadata().getName()); + return super.create(desired, nessie, context); + } + + public abstract static class ActivationCondition + implements Condition { + + private final String networkingVersion; + + protected ActivationCondition(String networkingVersion) { + this.networkingVersion = networkingVersion; + } + + @Override + public boolean isMet( + DependentResource dependentResource, Nessie nessie, Context context) { + boolean conditionMet = false; + if (nessie.getSpec().ingress().enabled()) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + conditionMet = helper.isApiSupported("networking.k8s.io", networkingVersion); + } + LOGGER.debug("Ingress {} activation condition met? {}", networkingVersion, conditionMet); + return conditionMet; + } + } + + public abstract static class ReadyCondition + implements Condition { + + private final Class resourceClass; + + protected ReadyCondition(Class resourceClass) { + this.resourceClass = resourceClass; + } + + @Override + public boolean isMet( + DependentResource dependentResource, Nessie nessie, Context context) { + boolean conditionMet = + context.getSecondaryResource(resourceClass).map(this::checkIngressReady).orElse(false); + LOGGER.debug("Ingress is ready? {}", conditionMet); + return conditionMet; + } + + protected abstract boolean checkIngressReady(I ingress); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractServiceAccountDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractServiceAccountDependent.java new file mode 100644 index 00000000000..86d94b185f2 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/AbstractServiceAccountDependent.java @@ -0,0 +1,94 @@ +/* + * 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.CreatingServiceAccount; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.ServiceAccount; +import io.fabric8.kubernetes.api.model.ServiceAccountBuilder; +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.workflow.Condition; +import org.projectnessie.operator.events.EventService; +import org.projectnessie.operator.reconciler.KubernetesHelper; +import org.projectnessie.operator.reconciler.nessie.resource.options.ServiceAccountOptions; +import org.projectnessie.operator.utils.ResourceUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractServiceAccountDependent

+ extends CRUDKubernetesDependentResource { + + private static final Logger LOGGER = + LoggerFactory.getLogger(AbstractServiceAccountDependent.class); + + public AbstractServiceAccountDependent() { + super(ServiceAccount.class); + } + + @Override + public ServiceAccount create(ServiceAccount desired, P primary, Context

context) { + LOGGER.debug( + "Creating service account {} for {} {}", + desired.getMetadata().getName(), + primary.getSingular(), + primary.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + primary, + CreatingServiceAccount, + "Creating service account %s", + desired.getMetadata().getName()); + return super.create(desired, primary, context); + } + + protected ServiceAccount desired( + P primary, ServiceAccountOptions serviceAccount, Context

context) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + ObjectMeta metadata = + helper + .metaBuilder(primary, serviceAccountName(primary, serviceAccount)) + .withAnnotations(serviceAccount.annotations()) + .build(); + return new ServiceAccountBuilder().withMetadata(metadata).build(); + } + + public static String serviceAccountName( + HasMetadata primary, ServiceAccountOptions serviceAccount) { + if (serviceAccount.name() != null) { + ResourceUtils.validateName(serviceAccount.name()); + return serviceAccount.name(); + } else if (serviceAccount.create()) { + return primary.getMetadata().getName(); + } + return "default"; + } + + public abstract static class ActivationCondition

+ implements Condition { + + @Override + public boolean isMet( + DependentResource dependentResource, P primary, Context

context) { + return serviceAccount(primary).create(); + } + + protected abstract ServiceAccountOptions serviceAccount(P primary); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ConfigMapDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ConfigMapDependent.java new file mode 100644 index 00000000000..2d69918b3fe --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ConfigMapDependent.java @@ -0,0 +1,309 @@ +/* + * 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 java.nio.charset.StandardCharsets.UTF_8; +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; + +import com.fasterxml.jackson.databind.JsonNode; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.bouncycastle.util.encoders.Hex; +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.NessieSpec.LogLevel; +import org.projectnessie.operator.reconciler.nessie.resource.options.AuthorizationOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.BigTableOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.CassandraOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.DynamoDbOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.JdbcOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.JdbcOptions.DataSource; +import org.projectnessie.operator.reconciler.nessie.resource.options.MongoDbOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.VersionStoreCacheOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.VersionStoreOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.VersionStoreOptions.VersionStoreType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class ConfigMapDependent extends CRUDKubernetesDependentResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigMapDependent.class); + + private static final long MIB = 1024L * 1024L; + + private static final String FILE_HEADER = + """ + # Automatically generated by the Nessie Kubernetes Operator + # DO NOT EDIT + + """; + + public ConfigMapDependent() { + super(ConfigMap.class); + } + + @Override + public ConfigMap create(ConfigMap desired, Nessie nessie, Context context) { + LOGGER.debug( + "Creating config-map {} for {}", + desired.getMetadata().getName(), + nessie.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + nessie, CreatingConfigMap, "Creating config-map %s", desired.getMetadata().getName()); + return super.create(desired, nessie, context); + } + + @Override + public ConfigMap desired(Nessie nessie, Context context) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + return new ConfigMapBuilder() + .withMetadata(helper.metaBuilder(nessie).build()) + .addToData("application.properties", FILE_HEADER + configAsProperties(nessie)) + .build(); + } + + private static Map collectConfig(Nessie nessie) { + Map config = new TreeMap<>(); + configureLogLevel(nessie, config); + configureVersionStore(nessie, config); + configureAuthentication(nessie, config); + configureAuthorization(nessie, config); + configureTelemetry(nessie, config); + configureAdvancedConfig(nessie, config); + return config; + } + + public static String configChecksum(Nessie nessie) { + Map config = collectConfig(nessie); + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + config.forEach( + (k, v) -> { + digest.update(k.getBytes(UTF_8)); + digest.update(v.getBytes(UTF_8)); + }); + String checksum = new String(Hex.encode(digest.digest())); + return "sha256:" + checksum; + } + + private static String configAsProperties(Nessie nessie) { + Map config = collectConfig(nessie); + StringBuilder sb = new StringBuilder(); + config.forEach((k, v) -> sb.append(k).append("=").append(v).append("\n")); + return sb.toString(); + } + + private static void configureLogLevel(Nessie nessie, Map config) { + LogLevel logLevel = nessie.getSpec().logLevel(); + if (logLevel != LogLevel.INFO) { + config.put("quarkus.log.level", logLevel.name()); + } + if (logLevel.compareTo(LogLevel.INFO) < 0) { + config.put("quarkus.log.console.level", logLevel.name()); + config.put("quarkus.log.file.level", logLevel.name()); + } + } + + private static void configureAuthentication(Nessie nessie, Map config) { + if (nessie.getSpec().authentication().enabled()) { + config.put("nessie.server.authentication.enabled", "true"); + String oidcAuthServerUrl = nessie.getSpec().authentication().oidcAuthServerUrl(); + config.put("quarkus.oidc.auth-server-url", oidcAuthServerUrl); + String oidcClientId = nessie.getSpec().authentication().oidcClientId(); + config.put("quarkus.oidc.client-id", oidcClientId); + } else { + config.put("quarkus.oidc.tenant-enabled", "false"); + } + } + + private static void configureAuthorization(Nessie nessie, Map config) { + AuthorizationOptions authorization = nessie.getSpec().authorization(); + if (authorization.enabled()) { + config.put("nessie.server.authorization.enabled", "true"); + authorization + .rules() + .forEach((key, value) -> config.put("nessie.server.authorization.rules." + key, value)); + } + } + + private static void configureTelemetry(Nessie nessie, Map config) { + if (nessie.getSpec().telemetry().enabled()) { + String endpoint = nessie.getSpec().telemetry().endpoint(); + config.put("quarkus.otel.exporter.otlp.traces.endpoint", endpoint); + Map attributes = + new LinkedHashMap<>(nessie.getSpec().telemetry().attributes()); + attributes.putIfAbsent("service.name", nessie.getMetadata().getName()); + String attributesStr = + attributes.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .reduce((a, b) -> a + "," + b) + .orElse(""); + config.put("quarkus.otel.resource.attributes", attributesStr); + String sample = nessie.getSpec().telemetry().sample(); + if (sample != null && !sample.isEmpty()) { + switch (sample) { + case "all" -> config.put("quarkus.otel.traces.sampler", "parentbased_always_on"); + case "none" -> config.put("quarkus.otel.traces.sampler", "parentbased_always_off"); + default -> { + config.put("quarkus.otel.traces.sampler", "parentbased_traceidratio"); + config.put("quarkus.otel.traces.sampler.arg", sample); + } + } + } + } else { + config.put("quarkus.otel.sdk.disabled", "true"); + } + } + + private static void configureVersionStore(Nessie nessie, Map config) { + VersionStoreOptions versionStore = nessie.getSpec().versionStore(); + configureVersionStoreCache(nessie, config); + VersionStoreType type = versionStore.type(); + switch (type) { + case InMemory -> {} + case RocksDb -> configureRocks(config); + case Jdbc -> configureJdbc(nessie, config); + case BigTable -> configureBigTable(nessie, config); + case MongoDb -> configureMongo(nessie, config); + case Cassandra -> configureCassandra(nessie, config); + case DynamoDb -> configureDynamo(nessie, config); + default -> throw new AssertionError("Unexpected version store type: " + type); + } + } + + private static void configureVersionStoreCache(Nessie nessie, Map config) { + VersionStoreCacheOptions cache = nessie.getSpec().versionStore().cache(); + if (cache.enabled()) { + if (cache.fixedSize() != null) { + long mb = cache.fixedSize().getNumericalAmount().longValue() / MIB; + config.put("nessie.version.store.persist.cache-capacity-mb", String.valueOf(mb)); + } else { + if (!cache.heapFraction().equals(VersionStoreCacheOptions.DEFAULT_HEAP_PERCENTAGE)) { + double hf = cache.heapFraction().getNumericalAmount().doubleValue(); + config.put( + "nessie.version.store.persist.cache-capacity-fraction-of-heap", String.valueOf(hf)); + } + if (!cache.minSize().equals(VersionStoreCacheOptions.DEFAULT_MIN_SIZE)) { + long ms = cache.minSize().getNumericalAmount().longValue() / MIB; + config.put( + "nessie.version.store.persist.cache-capacity-fraction-min-size-mb", + String.valueOf(ms)); + } + if (!cache.minFreeHeap().equals(VersionStoreCacheOptions.DEFAULT_MIN_FREE_HEAP)) { + long mfh = cache.minFreeHeap().getNumericalAmount().longValue() / MIB; + config.put( + "nessie.version.store.persist.cache-capacity-fraction-adjust-mb", + String.valueOf(mfh)); + } + } + } else { + config.put("nessie.version.store.persist.cache-capacity-mb", "0"); + } + } + + private static void configureRocks(Map config) { + config.put("nessie.version.store.type", "ROCKSDB"); + config.put( + "nessie.version.store.persist.rocks.database-path", DeploymentDependent.ROCKS_MOUNT_PATH); + } + + private static void configureJdbc(Nessie nessie, Map config) { + JdbcOptions jdbc = Objects.requireNonNull(nessie.getSpec().versionStore().jdbc()); + config.put("nessie.version.store.type", "JDBC"); + DataSource datasource = jdbc.datasource(); + config.put("nessie.version.store.persist.jdbc.datasource", datasource.name()); + config.put(datasource.configPrefix() + "jdbc.url", jdbc.url()); + if (jdbc.username() != null) { + config.put(datasource.configPrefix() + "username", jdbc.username()); + } + } + + private static void configureBigTable(Nessie nessie, Map config) { + BigTableOptions bigTable = Objects.requireNonNull(nessie.getSpec().versionStore().bigTable()); + config.put("nessie.version.store.type", "BIGTABLE"); + config.put("quarkus.google.cloud.project-id", bigTable.projectId()); + config.put("nessie.version.store.persist.bigtable.instance-id", bigTable.instanceId()); + config.put("nessie.version.store.persist.bigtable.app-profile-id", bigTable.appProfileId()); + } + + private static void configureMongo(Nessie nessie, Map config) { + MongoDbOptions mongoDb = Objects.requireNonNull(nessie.getSpec().versionStore().mongoDb()); + config.put("nessie.version.store.type", "MONGODB"); + config.put("quarkus.mongodb.connection-string", mongoDb.connectionString()); + config.put("quarkus.mongodb.database", mongoDb.database()); + if (mongoDb.username() != null) { + config.put("quarkus.mongodb.credentials.username", mongoDb.username()); + } + } + + private static void configureCassandra(Nessie nessie, Map config) { + CassandraOptions cassandra = + Objects.requireNonNull(nessie.getSpec().versionStore().cassandra()); + config.put("nessie.version.store.type", "CASSANDRA"); + config.put("quarkus.cassandra.keyspace", cassandra.keyspace()); + config.put( + "quarkus.cassandra.contact-points", + cassandra.contactPoints().stream().reduce((a, b) -> a + "," + b).orElse("")); + config.put("quarkus.cassandra.local-datacenter", cassandra.localDatacenter()); + if (cassandra.username() != null) { + config.put("quarkus.cassandra.auth.username", cassandra.username()); + } + } + + private static void configureDynamo(Nessie nessie, Map config) { + DynamoDbOptions dynamoDb = Objects.requireNonNull(nessie.getSpec().versionStore().dynamoDb()); + config.put("nessie.version.store.type", "DYNAMODB"); + config.put("quarkus.dynamodb.aws.region", dynamoDb.region()); + } + + private static void configureAdvancedConfig(Nessie nessie, Map config) { + JsonNode advancedConfig = nessie.getSpec().advancedConfig(); + if (advancedConfig != null && !advancedConfig.isEmpty()) { + applyAdvancedConfig(config, advancedConfig, ""); + } + } + + private static void applyAdvancedConfig( + Map config, JsonNode configNode, String prefix) { + for (Map.Entry entry : configNode.properties()) { + String key = prefix + entry.getKey(); + JsonNode value = entry.getValue(); + if (value.isObject()) { + applyAdvancedConfig(config, value, key + "."); + } else { + assert value.isValueNode(); // already validated + config.put(key, value.asText()); + } + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/DeploymentDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/DeploymentDependent.java new file mode 100644 index 00000000000..07558e08959 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/DeploymentDependent.java @@ -0,0 +1,442 @@ +/* + * 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.DuplicateEnvVar; +import static org.projectnessie.operator.reconciler.nessie.dependent.ServiceAccountDependent.serviceAccountName; + +import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.ContainerPortBuilder; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.fabric8.kubernetes.api.model.EnvVarBuilder; +import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder; +import io.fabric8.kubernetes.api.model.HTTPGetActionBuilder; +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.KeyToPathBuilder; +import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimVolumeSource; +import io.fabric8.kubernetes.api.model.PodSpec; +import io.fabric8.kubernetes.api.model.PodSpecBuilder; +import io.fabric8.kubernetes.api.model.PodTemplateSpec; +import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; +import io.fabric8.kubernetes.api.model.ProbeBuilder; +import io.fabric8.kubernetes.api.model.SecretKeySelectorBuilder; +import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; +import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder; +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 java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; +import org.projectnessie.operator.events.EventReason; +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.AwsCredentials; +import org.projectnessie.operator.reconciler.nessie.resource.options.BigTableOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.CassandraOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.DynamoDbOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.ImageOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.JdbcOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.MongoDbOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.SecretValue; +import org.projectnessie.operator.reconciler.nessie.resource.options.ServiceOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.VersionStoreOptions.VersionStoreType; +import org.projectnessie.operator.reconciler.nessie.resource.options.WorkloadOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class DeploymentDependent extends CRUDKubernetesDependentResource { + + public static final String CONFIG_CHECKSUM_ANNOTATION = "projectnessie.org/config-checksum"; + public static final String ROCKS_MOUNT_PATH = "/rocks-nessie"; + + private static final Logger LOGGER = LoggerFactory.getLogger(DeploymentDependent.class); + + public DeploymentDependent() { + super(Deployment.class); + } + + @Override + public Deployment create(Deployment desired, Nessie nessie, Context context) { + LOGGER.debug( + "Creating deployment {} for {}", + desired.getMetadata().getName(), + nessie.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + nessie, + EventReason.CreatingDeployment, + "Creating deployment %s", + desired.getMetadata().getName()); + return super.create(desired, nessie, context); + } + + public Deployment desired(Nessie nessie, Context context) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + Deployment deployment = + new DeploymentBuilder() + .withMetadata( + helper + .metaBuilder(nessie) + // also apply pod labels to the deployment (but not pod annotations) + .addToLabels(nessie.getSpec().deployment().labels()) + .build()) + .withSpec(newDeploymentSpec(nessie, helper)) + .build(); + configureConfigMapMount(nessie, deployment); + configureAuthentication(nessie, deployment); + configureVersionStore(nessie, deployment); + configureEnvVars(nessie, deployment, context); + return deployment; + } + + private DeploymentSpec newDeploymentSpec(Nessie nessie, KubernetesHelper helper) { + Map selectorLabels = helper.selectorLabels(nessie); + return new DeploymentSpecBuilder() + .withSelector(new LabelSelectorBuilder().withMatchLabels(selectorLabels).build()) + .withReplicas(nessie.getSpec().autoscaling().enabled() ? null : nessie.getSpec().size()) + .withTemplate(newPodTemplateSpec(nessie, selectorLabels)) + .build(); + } + + private PodTemplateSpec newPodTemplateSpec(Nessie nessie, Map selectorLabels) { + WorkloadOptions pod = nessie.getSpec().deployment(); + return new PodTemplateSpecBuilder() + .withMetadata( + new ObjectMetaBuilder() + .withLabels(selectorLabels) + .addToLabels(pod.labels()) + .withAnnotations(pod.annotations()) + .addToAnnotations( + CONFIG_CHECKSUM_ANNOTATION, ConfigMapDependent.configChecksum(nessie)) + .build()) + .withSpec(newPodSpec(nessie)) + .build(); + } + + private PodSpec newPodSpec(Nessie nessie) { + WorkloadOptions pod = nessie.getSpec().deployment(); + return new PodSpecBuilder() + .withServiceAccountName(serviceAccountName(nessie, pod.serviceAccount())) + .withSecurityContext(pod.podSecurityContext()) + .withImagePullSecrets( + pod.image().pullSecretRef() != null ? List.of(pod.image().pullSecretRef()) : List.of()) + .withNodeSelector(pod.nodeSelector()) + .withTolerations(pod.tolerations()) + .withAffinity(pod.affinity()) + .withContainers(newContainer(nessie)) + .build(); + } + + private Container newContainer(Nessie nessie) { + WorkloadOptions pod = nessie.getSpec().deployment(); + ContainerBuilder containerBuilder = + new ContainerBuilder() + .withName("nessie") + .withImage(pod.image().fullName(ImageOptions.DEFAULT_NESSIE_REPOSITORY)) + .withImagePullPolicy(Objects.requireNonNull(pod.image().pullPolicy()).name()) + .withResources(pod.resources()) + .withSecurityContext(pod.containerSecurityContext()) + .withPorts( + new ContainerPortBuilder() + .withName(MainServiceDependent.PORT_NAME) + .withContainerPort(ServiceOptions.DEFAULT_NESSIE_PORT) + .withProtocol("TCP") + .build(), + new ContainerPortBuilder() + .withName(ManagementServiceDependent.PORT_NAME) + .withContainerPort(ManagementServiceDependent.PORT_NUMBER) + .withProtocol("TCP") + .build()) + .withLivenessProbe( + new ProbeBuilder() + .withHttpGet( + new HTTPGetActionBuilder() + .withPath("/q/health/live") + .withPort(new IntOrString(ManagementServiceDependent.PORT_NAME)) + .withScheme("HTTP") + .build()) + .withInitialDelaySeconds(pod.livenessProbe().initialDelaySeconds()) + .withPeriodSeconds(pod.livenessProbe().periodSeconds()) + .withTimeoutSeconds(pod.livenessProbe().timeoutSeconds()) + .withFailureThreshold(pod.livenessProbe().failureThreshold()) + .withSuccessThreshold(pod.livenessProbe().successThreshold()) + .build()) + .withReadinessProbe( + new ProbeBuilder() + .withHttpGet( + new HTTPGetActionBuilder() + .withPath("/q/health/ready") + .withPort(new IntOrString(ManagementServiceDependent.PORT_NAME)) + .withScheme("HTTP") + .build()) + .withInitialDelaySeconds(pod.readinessProbe().initialDelaySeconds()) + .withPeriodSeconds(pod.readinessProbe().periodSeconds()) + .withTimeoutSeconds(pod.readinessProbe().timeoutSeconds()) + .withFailureThreshold(pod.readinessProbe().failureThreshold()) + .withSuccessThreshold(pod.readinessProbe().successThreshold()) + .build()); + + if (nessie.getSpec().remoteDebug().enabled()) { + containerBuilder.addToPorts( + new ContainerPortBuilder() + .withContainerPort(nessie.getSpec().remoteDebug().port()) + .withName("nessie-debug") + .withProtocol("TCP") + .build()); + } + + return containerBuilder.build(); + } + + private static void configureConfigMapMount(Nessie nessie, Deployment deployment) { + PodSpec pod = deployment.getSpec().getTemplate().getSpec(); + Container container = pod.getContainers().get(0); + container + .getVolumeMounts() + .add( + new VolumeMountBuilder() + .withName("nessie-config") + .withMountPath("/deployments/config/application.properties") + .withSubPath("application.properties") + .build()); + pod.getVolumes() + .add( + new VolumeBuilder() + .withName("nessie-config") + .withConfigMap( + new ConfigMapVolumeSourceBuilder() + .withName(nessie.getMetadata().getName()) + .withOptional(false) + .build()) + .build()); + } + + private static void configureAuthentication(Nessie nessie, Deployment deployment) { + if (nessie.getSpec().authentication().enabled()) { + SecretValue secretValue = nessie.getSpec().authentication().oidcClientSecret(); + if (secretValue != null) { + PodSpec pod = deployment.getSpec().getTemplate().getSpec(); + Container container = pod.getContainers().get(0); + container.getEnv().add(envVarFromSecret("quarkus.oidc.credentials.secret", secretValue)); + } + } + } + + private static void configureVersionStore(Nessie nessie, Deployment deployment) { + PodSpec pod = deployment.getSpec().getTemplate().getSpec(); + Container container = pod.getContainers().get(0); + VersionStoreType type = nessie.getSpec().versionStore().type(); + switch (type) { + case InMemory -> {} + case RocksDb -> configureRocks(nessie, container, pod.getVolumes()); + case Jdbc -> configureJdbc(nessie, container); + case BigTable -> configureBigTable(nessie, container, pod.getVolumes()); + case MongoDb -> configureMongo(nessie, container); + case Cassandra -> configureCassandra(nessie, container); + case DynamoDb -> configureDynamo(nessie, container); + default -> throw new AssertionError("Unexpected version store type: " + type); + } + } + + private static void configureRocks(Nessie nessie, Container container, List volumes) { + container + .getVolumeMounts() + .add( + new VolumeMountBuilder() + .withName("rocks-storage") + .withMountPath(ROCKS_MOUNT_PATH) + .build()); + // Note: readOnly: false creates an infinite reconcile loop, because the actual deployment + // will contain readOnly: null regardless of the value in the desired deployment. + PersistentVolumeClaimVolumeSource claim = + new PersistentVolumeClaimVolumeSource(nessie.getMetadata().getName(), null); + volumes.add( + new VolumeBuilder().withName("rocks-storage").withPersistentVolumeClaim(claim).build()); + } + + private static void configureJdbc(Nessie nessie, Container container) { + JdbcOptions jdbc = nessie.getSpec().versionStore().jdbc(); + if (jdbc != null && jdbc.password() != null) { + EnvVar password = + envVarFromSecret(jdbc.datasource().configPrefix() + "password", jdbc.password()); + container.getEnv().add(password); + } + } + + private static void configureBigTable(Nessie nessie, Container container, List volumes) { + BigTableOptions bigTable = nessie.getSpec().versionStore().bigTable(); + if (bigTable != null && bigTable.credentials() != null) { + container + .getEnv() + .add( + new EnvVar( + "GOOGLE_APPLICATION_CREDENTIALS", "/bigtable-nessie/sa_credentials.json", null)); + container + .getVolumeMounts() + .add( + new VolumeMountBuilder() + .withName("bigtable-creds") + .withMountPath("/bigtable-nessie") + .build()); + volumes.add( + new VolumeBuilder() + .withName("bigtable-creds") + .withSecret( + new SecretVolumeSourceBuilder() + .withSecretName(bigTable.credentials().secret()) + .withItems( + new KeyToPathBuilder() + .withKey(bigTable.credentials().key()) + .withPath("sa_credentials.json") + .build()) + .build()) + .build()); + } + } + + private static void configureMongo(Nessie nessie, Container container) { + MongoDbOptions mongoDb = nessie.getSpec().versionStore().mongoDb(); + if (mongoDb != null && mongoDb.password() != null) { + container + .getEnv() + .add(envVarFromSecret("quarkus.mongodb.credentials.password", mongoDb.password())); + } + } + + private static void configureCassandra(Nessie nessie, Container container) { + CassandraOptions cassandra = nessie.getSpec().versionStore().cassandra(); + if (cassandra != null && cassandra.password() != null) { + container + .getEnv() + .add(envVarFromSecret("quarkus.cassandra.auth.password", cassandra.password())); + } + } + + private static void configureDynamo(Nessie nessie, Container container) { + DynamoDbOptions dynamoDb = nessie.getSpec().versionStore().dynamoDb(); + if (dynamoDb != null) { + AwsCredentials credentials = dynamoDb.credentials(); + container + .getEnv() + .add( + envVarFromSecret( + "AWS_ACCESS_KEY_ID", credentials.secret(), credentials.accessKeyId())); + container + .getEnv() + .add( + envVarFromSecret( + "AWS_SECRET_ACCESS_KEY", credentials.secret(), credentials.secretAccessKey())); + } + } + + private static void configureEnvVars( + Nessie nessie, Deployment deployment, Context context) { + List env = new ArrayList<>(); + addJvmOptionsEnvVar(nessie, env); + addDebugEnvVars(nessie, env); + addExtraEnvVars(nessie, env); + Map map = new TreeMap<>(); + for (EnvVar envVar : env) { + EnvVar old = map.put(envVar.getName(), envVar); + if (old != null) { + EventService.retrieveFromContext(context) + .fireEvent( + nessie, DuplicateEnvVar, "Duplicate environment variable: %s", envVar.getName()); + } + } + deployment + .getSpec() + .getTemplate() + .getSpec() + .getContainers() + .get(0) + .getEnv() + .addAll(map.values()); + } + + private static void addJvmOptionsEnvVar(Nessie nessie, List env) { + nessie.getSpec().jvmOptions().stream() + .map(Objects::toString) + .reduce((a, b) -> a + " " + b) + .ifPresent(s -> env.add(new EnvVar("JAVA_OPTS_APPEND", s, null))); + } + + private static void addDebugEnvVars(Nessie nessie, List env) { + if (nessie.getSpec().remoteDebug().enabled()) { + env.add(new EnvVar("JAVA_DEBUG", "true", null)); + // Use * to bind to all interfaces + env.add(new EnvVar("JAVA_DEBUG_PORT", "*:" + nessie.getSpec().remoteDebug().port(), null)); + } + } + + private static void addExtraEnvVars(Nessie nessie, List env) { + if (nessie.getSpec().extraEnv() != null) { + env.addAll(nessie.getSpec().extraEnv()); + } + } + + private static EnvVar envVarFromSecret(String name, SecretValue secretValue) { + return envVarFromSecret(name, secretValue.secret(), secretValue.key()); + } + + private static EnvVar envVarFromSecret(String name, String secretRef, String key) { + return new EnvVarBuilder() + .withName(name) + .withValueFrom( + new EnvVarSourceBuilder() + .withSecretKeyRef( + new SecretKeySelectorBuilder().withName(secretRef).withKey(key).build()) + .build()) + .build(); + } + + public static class ReadyCondition implements Condition { + + @Override + public boolean isMet( + DependentResource dependentResource, + Nessie nessie, + Context context) { + return dependentResource + .getSecondaryResource(nessie, context) + .map( + d -> + nessie.getSpec().autoscaling().enabled() + || (d.getStatus() != null + && Objects.equals( + d.getStatus().getAvailableReplicas(), nessie.getSpec().size()))) + .orElse(false); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Beta1Dependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Beta1Dependent.java new file mode 100644 index 00000000000..f818616a254 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Beta1Dependent.java @@ -0,0 +1,88 @@ +/* + * 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 io.fabric8.kubernetes.api.model.autoscaling.v2beta1.CrossVersionObjectReferenceBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta1.HorizontalPodAutoscaler; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta1.HorizontalPodAutoscalerBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta1.HorizontalPodAutoscalerSpecBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta1.MetricSpec; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta1.MetricSpecBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta1.ResourceMetricSourceBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +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.AutoscalingOptions; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class HorizontalPodAutoscalerV2Beta1Dependent + extends AbstractHorizontalPodAutoscalerDependent { + + public HorizontalPodAutoscalerV2Beta1Dependent() { + super(HorizontalPodAutoscaler.class); + } + + @Override + protected HorizontalPodAutoscaler desired(Nessie nessie, Context context) { + AutoscalingOptions autoscaling = nessie.getSpec().autoscaling(); + HorizontalPodAutoscalerSpecBuilder specBuilder = + new HorizontalPodAutoscalerSpecBuilder() + .withScaleTargetRef( + new CrossVersionObjectReferenceBuilder() + .withApiVersion("apps/v1") + .withKind("Deployment") + .withName(nessie.getMetadata().getName()) + .build()) + .withMinReplicas(autoscaling.minReplicas()) + .withMaxReplicas(autoscaling.maxReplicas()); + Integer cpu = autoscaling.targetCpuUtilizationPercentage(); + if (cpu != null && cpu > 0) { + specBuilder.addToMetrics(metric("cpu", cpu)); + } + Integer memory = autoscaling.targetMemoryUtilizationPercentage(); + if (memory != null && memory > 0) { + specBuilder.addToMetrics(metric("memory", memory)); + } + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + return new HorizontalPodAutoscalerBuilder() + .withMetadata(helper.metaBuilder(nessie).build()) + .withSpec(specBuilder.build()) + .build(); + } + + private static MetricSpec metric(String name, int percentage) { + return new MetricSpecBuilder() + .withType("Resource") + .withResource( + new ResourceMetricSourceBuilder() + .withName(name) + .withTargetAverageUtilization(percentage) + .build()) + .build(); + } + + public static class ActivationCondition + extends AbstractHorizontalPodAutoscalerDependent.ActivationCondition< + HorizontalPodAutoscaler> { + + @Override + protected boolean isAutoscalingSupported(KubernetesHelper helper) { + return helper.isAutoscalingV2Beta1Supported(); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Beta2Dependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Beta2Dependent.java new file mode 100644 index 00000000000..586097eb0cc --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Beta2Dependent.java @@ -0,0 +1,93 @@ +/* + * 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 io.fabric8.kubernetes.api.model.autoscaling.v2beta2.CrossVersionObjectReferenceBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta2.HorizontalPodAutoscaler; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta2.HorizontalPodAutoscalerBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta2.HorizontalPodAutoscalerSpecBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta2.MetricSpec; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta2.MetricSpecBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta2.MetricTargetBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2beta2.ResourceMetricSourceBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +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.AutoscalingOptions; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class HorizontalPodAutoscalerV2Beta2Dependent + extends AbstractHorizontalPodAutoscalerDependent { + + public HorizontalPodAutoscalerV2Beta2Dependent() { + super(HorizontalPodAutoscaler.class); + } + + @Override + protected HorizontalPodAutoscaler desired(Nessie nessie, Context context) { + AutoscalingOptions autoscaling = nessie.getSpec().autoscaling(); + HorizontalPodAutoscalerSpecBuilder specBuilder = + new HorizontalPodAutoscalerSpecBuilder() + .withScaleTargetRef( + new CrossVersionObjectReferenceBuilder() + .withApiVersion("apps/v1") + .withKind("Deployment") + .withName(nessie.getMetadata().getName()) + .build()) + .withMinReplicas(autoscaling.minReplicas()) + .withMaxReplicas(autoscaling.maxReplicas()); + Integer cpu = autoscaling.targetCpuUtilizationPercentage(); + if (cpu != null && cpu > 0) { + specBuilder.addToMetrics(metric("cpu", cpu)); + } + Integer memory = autoscaling.targetMemoryUtilizationPercentage(); + if (memory != null && memory > 0) { + specBuilder.addToMetrics(metric("memory", memory)); + } + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + return new HorizontalPodAutoscalerBuilder() + .withMetadata(helper.metaBuilder(nessie).build()) + .withSpec(specBuilder.build()) + .build(); + } + + private static MetricSpec metric(String name, int percentage) { + return new MetricSpecBuilder() + .withType("Resource") + .withResource( + new ResourceMetricSourceBuilder() + .withName(name) + .withTarget( + new MetricTargetBuilder() + .withType("Utilization") + .withAverageUtilization(percentage) + .build()) + .build()) + .build(); + } + + public static class ActivationCondition + extends AbstractHorizontalPodAutoscalerDependent.ActivationCondition< + HorizontalPodAutoscaler> { + + @Override + protected boolean isAutoscalingSupported(KubernetesHelper helper) { + return helper.isAutoscalingV2Beta2Supported(); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Dependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Dependent.java new file mode 100644 index 00000000000..ec7450a2e70 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/HorizontalPodAutoscalerV2Dependent.java @@ -0,0 +1,93 @@ +/* + * 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 io.fabric8.kubernetes.api.model.autoscaling.v2.CrossVersionObjectReferenceBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2.HorizontalPodAutoscaler; +import io.fabric8.kubernetes.api.model.autoscaling.v2.HorizontalPodAutoscalerBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2.HorizontalPodAutoscalerSpecBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2.MetricSpec; +import io.fabric8.kubernetes.api.model.autoscaling.v2.MetricSpecBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2.MetricTargetBuilder; +import io.fabric8.kubernetes.api.model.autoscaling.v2.ResourceMetricSourceBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +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.AutoscalingOptions; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class HorizontalPodAutoscalerV2Dependent + extends AbstractHorizontalPodAutoscalerDependent { + + public HorizontalPodAutoscalerV2Dependent() { + super(HorizontalPodAutoscaler.class); + } + + @Override + protected HorizontalPodAutoscaler desired(Nessie nessie, Context context) { + AutoscalingOptions autoscaling = nessie.getSpec().autoscaling(); + HorizontalPodAutoscalerSpecBuilder specBuilder = + new HorizontalPodAutoscalerSpecBuilder() + .withScaleTargetRef( + new CrossVersionObjectReferenceBuilder() + .withApiVersion("apps/v1") + .withKind("Deployment") + .withName(nessie.getMetadata().getName()) + .build()) + .withMinReplicas(autoscaling.minReplicas()) + .withMaxReplicas(autoscaling.maxReplicas()); + Integer cpu = autoscaling.targetCpuUtilizationPercentage(); + if (cpu != null && cpu > 0) { + specBuilder.addToMetrics(metric("cpu", cpu)); + } + Integer memory = autoscaling.targetMemoryUtilizationPercentage(); + if (memory != null && memory > 0) { + specBuilder.addToMetrics(metric("memory", memory)); + } + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + return new HorizontalPodAutoscalerBuilder() + .withMetadata(helper.metaBuilder(nessie).build()) + .withSpec(specBuilder.build()) + .build(); + } + + private static MetricSpec metric(String name, int percentage) { + return new MetricSpecBuilder() + .withType("Resource") + .withResource( + new ResourceMetricSourceBuilder() + .withName(name) + .withTarget( + new MetricTargetBuilder() + .withType("Utilization") + .withAverageUtilization(percentage) + .build()) + .build()) + .build(); + } + + public static class ActivationCondition + extends AbstractHorizontalPodAutoscalerDependent.ActivationCondition< + HorizontalPodAutoscaler> { + + @Override + protected boolean isAutoscalingSupported(KubernetesHelper helper) { + return helper.isAutoscalingV2Supported(); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/IngressV1Beta1Dependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/IngressV1Beta1Dependent.java new file mode 100644 index 00000000000..29cccf19b08 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/IngressV1Beta1Dependent.java @@ -0,0 +1,134 @@ +/* + * 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 io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.networking.v1beta1.Ingress; +import io.fabric8.kubernetes.api.model.networking.v1beta1.IngressBuilder; +import io.fabric8.kubernetes.api.model.networking.v1beta1.IngressLoadBalancerIngress; +import io.fabric8.kubernetes.api.model.networking.v1beta1.IngressRuleBuilder; +import io.fabric8.kubernetes.api.model.networking.v1beta1.IngressSpecBuilder; +import io.fabric8.kubernetes.api.model.networking.v1beta1.IngressStatus; +import io.fabric8.kubernetes.api.model.networking.v1beta1.IngressTLSBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import java.util.List; +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.IngressOptions.Rule; +import org.projectnessie.operator.reconciler.nessie.resource.options.IngressOptions.Tls; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class IngressV1Beta1Dependent extends AbstractIngressDependent { + + public IngressV1Beta1Dependent() { + super(Ingress.class); + } + + @Override + public Ingress desired(Nessie nessie, Context context) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + ObjectMeta metadata = + helper + .metaBuilder(nessie) + .withAnnotations(nessie.getSpec().ingress().annotations()) + .build(); + Ingress ingress = + new IngressBuilder() + .withMetadata(metadata) + .withSpec( + new IngressSpecBuilder() + .withIngressClassName(nessie.getSpec().ingress().ingressClassName()) + .build()) + .build(); + configureRules(ingress, nessie); + configureTls(ingress, nessie); + return ingress; + } + + private void configureRules(Ingress ingress, Nessie nessie) { + List rules = nessie.getSpec().ingress().rules(); + for (Rule rule : rules) { + IngressRuleBuilder ruleBuilder = new IngressRuleBuilder(); + ruleBuilder.withHost(rule.host()); + for (String path : rule.paths()) { + ruleBuilder + .withNewHttp() + .withPaths() + .addNewPath() + .withPath(path) + .withPathType("ImplementationSpecific") + .withNewBackend() + .withServiceName(nessie.getMetadata().getName()) + .withNewServicePort() + .withValue(nessie.getSpec().service().port()) + .endServicePort() + .endBackend() + .endPath() + .endHttp(); + } + ingress.getSpec().getRules().add(ruleBuilder.build()); + } + } + + private void configureTls(Ingress ingress, Nessie nessie) { + for (Tls tls : nessie.getSpec().ingress().tls()) { + IngressTLSBuilder tlsBuilder = new IngressTLSBuilder(); + tlsBuilder.withHosts(tls.hosts()); + tlsBuilder.withSecretName(tls.secret()); + ingress.getSpec().getTls().add(tlsBuilder.build()); + } + } + + public static String getExposedUrl(Ingress ingress) { + IngressLoadBalancerIngress ing = ingress.getStatus().getLoadBalancer().getIngress().get(0); + return "https://" + (ing.getHostname() != null ? ing.getHostname() : ing.getIp()); + } + + public static void updateStatus(Nessie nessie, Context context) { + context + .getSecondaryResource(Ingress.class) + .ifPresentOrElse( + ingress -> nessie.getStatus().setExposedUrl(getExposedUrl(ingress)), + () -> nessie.getStatus().setExposedUrl(null)); + } + + public static class ActivationCondition + extends AbstractIngressDependent.ActivationCondition { + + public ActivationCondition() { + super("v1beta1"); + } + } + + public static class ReadyCondition extends AbstractIngressDependent.ReadyCondition { + + public ReadyCondition() { + super(Ingress.class); + } + + @Override + protected boolean checkIngressReady(Ingress ingress) { + IngressStatus status = ingress.getStatus(); + if (status != null) { + List ingresses = status.getLoadBalancer().getIngress(); + return ingresses != null && !ingresses.isEmpty() && ingresses.get(0).getIp() != null; + } + return false; + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/IngressV1Dependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/IngressV1Dependent.java new file mode 100644 index 00000000000..eb2a578929e --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/IngressV1Dependent.java @@ -0,0 +1,136 @@ +/* + * 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 io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.networking.v1.Ingress; +import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder; +import io.fabric8.kubernetes.api.model.networking.v1.IngressLoadBalancerIngress; +import io.fabric8.kubernetes.api.model.networking.v1.IngressRuleBuilder; +import io.fabric8.kubernetes.api.model.networking.v1.IngressSpecBuilder; +import io.fabric8.kubernetes.api.model.networking.v1.IngressStatus; +import io.fabric8.kubernetes.api.model.networking.v1.IngressTLSBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import java.util.List; +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.IngressOptions.Rule; +import org.projectnessie.operator.reconciler.nessie.resource.options.IngressOptions.Tls; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class IngressV1Dependent extends AbstractIngressDependent { + + protected IngressV1Dependent() { + super(Ingress.class); + } + + @Override + public Ingress desired(Nessie nessie, Context context) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + ObjectMeta metadata = + helper + .metaBuilder(nessie) + .withAnnotations(nessie.getSpec().ingress().annotations()) + .build(); + Ingress ingress = + new IngressBuilder() + .withMetadata(metadata) + .withSpec( + new IngressSpecBuilder() + .withIngressClassName(nessie.getSpec().ingress().ingressClassName()) + .build()) + .build(); + configureRules(ingress, nessie); + configureTls(ingress, nessie); + return ingress; + } + + private void configureRules(Ingress ingress, Nessie nessie) { + List rules = nessie.getSpec().ingress().rules(); + for (Rule rule : rules) { + IngressRuleBuilder ruleBuilder = new IngressRuleBuilder(); + ruleBuilder.withHost(rule.host()); + for (String path : rule.paths()) { + ruleBuilder + .withNewHttp() + .withPaths() + .addNewPath() + .withPath(path) + .withPathType("ImplementationSpecific") + .withNewBackend() + .withNewService() + .withName(nessie.getMetadata().getName()) + .withNewPort() + .withNumber(nessie.getSpec().service().port()) + .endPort() + .endService() + .endBackend() + .endPath() + .endHttp(); + } + ingress.getSpec().getRules().add(ruleBuilder.build()); + } + } + + private void configureTls(Ingress ingress, Nessie nessie) { + for (Tls tls : nessie.getSpec().ingress().tls()) { + IngressTLSBuilder tlsBuilder = new IngressTLSBuilder(); + tlsBuilder.withHosts(tls.hosts()); + tlsBuilder.withSecretName(tls.secret()); + ingress.getSpec().getTls().add(tlsBuilder.build()); + } + } + + public static String getExposedUrl(Ingress ingress) { + IngressLoadBalancerIngress ing = ingress.getStatus().getLoadBalancer().getIngress().get(0); + return "https://" + (ing.getHostname() != null ? ing.getHostname() : ing.getIp()); + } + + public static void updateStatus(Nessie nessie, Context context) { + context + .getSecondaryResource(Ingress.class) + .ifPresentOrElse( + ingress -> nessie.getStatus().setExposedUrl(getExposedUrl(ingress)), + () -> nessie.getStatus().setExposedUrl(null)); + } + + public static class ActivationCondition + extends AbstractIngressDependent.ActivationCondition { + + public ActivationCondition() { + super("v1"); + } + } + + public static class ReadyCondition extends AbstractIngressDependent.ReadyCondition { + + public ReadyCondition() { + super(Ingress.class); + } + + @Override + protected boolean checkIngressReady(Ingress ingress) { + IngressStatus status = ingress.getStatus(); + if (status != null) { + List ingresses = status.getLoadBalancer().getIngress(); + return ingresses != null && !ingresses.isEmpty() && ingresses.get(0).getIp() != null; + } + return false; + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/MainServiceDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/MainServiceDependent.java new file mode 100644 index 00000000000..918e371b209 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/MainServiceDependent.java @@ -0,0 +1,103 @@ +/* + * 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.CreatingService; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import java.util.Optional; +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.dependent.MainServiceDependent.Discriminator; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.reconciler.nessie.resource.options.ServiceOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@KubernetesDependent( + labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR, + resourceDiscriminator = Discriminator.class) +public class MainServiceDependent extends CRUDKubernetesDependentResource { + + public static final String PORT_NAME = "nessie-server"; + + private static final Logger LOGGER = LoggerFactory.getLogger(MainServiceDependent.class); + + public MainServiceDependent() { + super(Service.class); + } + + @Override + public Service create(Service desired, Nessie nessie, Context context) { + LOGGER.debug( + "Creating service {} for {}", + desired.getMetadata().getName(), + nessie.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + nessie, CreatingService, "Creating service %s", desired.getMetadata().getName()); + return super.create(desired, nessie, context); + } + + @Override + public Service desired(Nessie nessie, Context context) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + ServiceOptions service = nessie.getSpec().service(); + return new ServiceBuilder() + .withMetadata( + helper + .metaBuilder(nessie) + .addToLabels(service.labels()) + .withAnnotations(service.annotations()) + .build()) + .withNewSpec() + .withType(service.type().name()) + .addNewPort() + .withName(PORT_NAME) + .withProtocol("TCP") + .withPort(service.port()) + .withNewTargetPort() + .withValue(ServiceOptions.DEFAULT_NESSIE_PORT) + .endTargetPort() + .withNodePort(service.nodePort()) + .endPort() + .withSelector(helper.selectorLabels(nessie)) + .withSessionAffinity(service.sessionAffinity().name()) + .endSpec() + .build(); + } + + public static class Discriminator implements ResourceDiscriminator { + + @Override + public Optional distinguish( + Class resource, Nessie primary, Context context) { + InformerEventSource ies = + (InformerEventSource) + context.eventSourceRetriever().getResourceEventSourceFor(Service.class); + return ies.get( + new ResourceID(primary.getMetadata().getName(), primary.getMetadata().getNamespace())); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ManagementServiceDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ManagementServiceDependent.java new file mode 100644 index 00000000000..140bdc0bc6f --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ManagementServiceDependent.java @@ -0,0 +1,112 @@ +/* + * 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.CreatingMgmtService; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import java.util.Optional; +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.dependent.ManagementServiceDependent.Discriminator; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.reconciler.nessie.resource.options.ServiceOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@KubernetesDependent( + labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR, + resourceDiscriminator = Discriminator.class) +public class ManagementServiceDependent extends CRUDKubernetesDependentResource { + + public static final int PORT_NUMBER = 9000; + + public static final String PORT_NAME = "nessie-mgmt"; + + public static final String SERVICE_NAME_SUFFIX = "-mgmt"; + + private static final Logger LOGGER = LoggerFactory.getLogger(ManagementServiceDependent.class); + + public ManagementServiceDependent() { + super(Service.class); + } + + @Override + public Service create(Service desired, Nessie nessie, Context context) { + LOGGER.debug( + "Creating management service {} for {}", + desired.getMetadata().getName(), + nessie.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + nessie, + CreatingMgmtService, + "Creating management service %s", + desired.getMetadata().getName() + SERVICE_NAME_SUFFIX); + return super.create(desired, nessie, context); + } + + @Override + public Service desired(Nessie nessie, Context context) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + ServiceOptions service = nessie.getSpec().service(); + return new ServiceBuilder() + .withMetadata( + helper + .metaBuilder(nessie, managementServiceName(nessie)) + .addToLabels(service.labels()) + .withAnnotations(service.annotations()) + .build()) + .withNewSpec() + .withClusterIP("None") + .addNewPort() + .withName(PORT_NAME) + .withProtocol("TCP") + .withPort(PORT_NUMBER) + .withNewTargetPort() + .withValue(PORT_NUMBER) + .endTargetPort() + .endPort() + .withSelector(helper.selectorLabels(nessie)) + .withPublishNotReadyAddresses() + .endSpec() + .build(); + } + + public static String managementServiceName(Nessie primary) { + return primary.getMetadata().getName() + SERVICE_NAME_SUFFIX; + } + + public static class Discriminator implements ResourceDiscriminator { + @Override + public Optional distinguish( + Class resource, Nessie primary, Context context) { + InformerEventSource ies = + (InformerEventSource) + context.eventSourceRetriever().getResourceEventSourceFor(Service.class); + return ies.get( + new ResourceID(managementServiceName(primary), primary.getMetadata().getNamespace())); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/PersistentVolumeClaimDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/PersistentVolumeClaimDependent.java new file mode 100644 index 00000000000..84bf1d8c68f --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/PersistentVolumeClaimDependent.java @@ -0,0 +1,118 @@ +/* + * 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.CreatingPersistentVolumeClaim; + +import io.fabric8.kubernetes.api.model.LabelSelectorBuilder; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimBuilder; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimSpec; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaimSpecBuilder; +import io.fabric8.kubernetes.api.model.VolumeResourceRequirementsBuilder; +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 java.util.Map; +import java.util.Objects; +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.RocksDbOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class PersistentVolumeClaimDependent + extends CRUDKubernetesDependentResource { + + private static final Logger LOGGER = + LoggerFactory.getLogger(PersistentVolumeClaimDependent.class); + + public PersistentVolumeClaimDependent() { + super(PersistentVolumeClaim.class); + } + + @Override + public PersistentVolumeClaim create( + PersistentVolumeClaim desired, Nessie nessie, Context context) { + LOGGER.debug( + "Creating pvc {} for {}", desired.getMetadata().getName(), nessie.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + nessie, CreatingPersistentVolumeClaim, "Creating PVC %s", desired.getMetadata().getName()); + return super.create(desired, nessie, context); + } + + @Override + public PersistentVolumeClaim desired(Nessie nessie, Context context) { + RocksDbOptions rocksDb = nessie.getSpec().versionStore().rocksDb(); + Objects.requireNonNull(rocksDb, "rocksDb config must not be null"); + PersistentVolumeClaimSpec volumeClaimSpec = + new PersistentVolumeClaimSpecBuilder() + .withAccessModes("ReadWriteOnce") + .withStorageClassName(rocksDb.storageClassName()) + .withResources( + new VolumeResourceRequirementsBuilder() + .withRequests(Map.of("storage", rocksDb.storageSize())) + .build()) + .build(); + if (rocksDb.selectorLabels() != null && !rocksDb.selectorLabels().isEmpty()) { + volumeClaimSpec.setSelector( + new LabelSelectorBuilder().withMatchLabels(rocksDb.selectorLabels()).build()); + } + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + return new PersistentVolumeClaimBuilder() + .withMetadata(helper.metaBuilder(nessie).build()) + .withSpec(volumeClaimSpec) + .build(); + } + + public static boolean isBound(PersistentVolumeClaim pvc) { + return pvc.getStatus() != null && Objects.equals(pvc.getStatus().getPhase(), "Bound"); + } + + public static class ActivationCondition implements Condition { + @Override + public boolean isMet( + DependentResource dependentResource, + Nessie nessie, + Context context) { + boolean conditionMet = nessie.getSpec().versionStore().type().requiresPvc(); + LOGGER.debug("PVC activation condition met: {}", conditionMet); + return conditionMet; + } + } + + public static class ReadyCondition implements Condition { + @Override + public boolean isMet( + DependentResource dependentResource, + Nessie nessie, + Context context) { + boolean conditionMet = + context + .getSecondaryResource(PersistentVolumeClaim.class) + .map(PersistentVolumeClaimDependent::isBound) + .orElse(false); + LOGGER.debug("PVC is ready: {}", conditionMet); + return conditionMet; + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ServiceAccountDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ServiceAccountDependent.java new file mode 100644 index 00000000000..20a09777278 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ServiceAccountDependent.java @@ -0,0 +1,41 @@ +/* + * 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 io.fabric8.kubernetes.api.model.ServiceAccount; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; +import org.projectnessie.operator.reconciler.nessie.NessieReconciler; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.reconciler.nessie.resource.options.ServiceAccountOptions; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class ServiceAccountDependent extends AbstractServiceAccountDependent { + + @Override + public ServiceAccount desired(Nessie nessie, Context context) { + return desired(nessie, nessie.getSpec().deployment().serviceAccount(), context); + } + + public static class ActivationCondition + extends AbstractServiceAccountDependent.ActivationCondition { + + @Override + protected ServiceAccountOptions serviceAccount(Nessie primary) { + return primary.getSpec().deployment().serviceAccount(); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ServiceMonitorDependent.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ServiceMonitorDependent.java new file mode 100644 index 00000000000..a3507b433ce --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/dependent/ServiceMonitorDependent.java @@ -0,0 +1,103 @@ +/* + * 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.CreatingServiceMonitor; +import static org.projectnessie.operator.events.EventReason.ServiceMonitorNotSupported; + +import io.fabric8.openshift.api.model.monitoring.v1.ServiceMonitor; +import io.fabric8.openshift.api.model.monitoring.v1.ServiceMonitorBuilder; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@KubernetesDependent(labelSelector = NessieReconciler.DEPENDENT_RESOURCES_SELECTOR) +public class ServiceMonitorDependent + extends CRUDKubernetesDependentResource { + + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceMonitorDependent.class); + + public ServiceMonitorDependent() { + super(ServiceMonitor.class); + } + + @Override + public ServiceMonitor create(ServiceMonitor desired, Nessie nessie, Context context) { + LOGGER.debug( + "Creating service monitor {} for {}", + desired.getMetadata().getName(), + nessie.getMetadata().getName()); + EventService eventService = EventService.retrieveFromContext(context); + eventService.fireEvent( + nessie, + CreatingServiceMonitor, + "Creating service monitor %s", + desired.getMetadata().getName()); + return super.create(desired, nessie, context); + } + + @Override + public ServiceMonitor desired(Nessie nessie, Context context) { + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + return new ServiceMonitorBuilder() + .withMetadata( + helper.metaBuilder(nessie).addToLabels(nessie.getSpec().monitoring().labels()).build()) + .withNewSpec() + .addNewEndpoint() + .withPort(ManagementServiceDependent.PORT_NAME) + .withScheme("http") + .withInterval(nessie.getSpec().monitoring().interval()) + .withPath("/q/metrics") + .endEndpoint() + .withNewNamespaceSelector() + .withMatchNames(nessie.getMetadata().getNamespace()) + .endNamespaceSelector() + .withNewSelector() + .withMatchLabels(helper.selectorLabels(nessie)) + .endSelector() + .endSpec() + .build(); + } + + public static class ActivationCondition implements Condition { + + @Override + public boolean isMet( + DependentResource dependentResource, + Nessie nessie, + Context context) { + boolean conditionMet = nessie.getSpec().monitoring().enabled(); + KubernetesHelper helper = KubernetesHelper.retrieveFromContext(context); + if (conditionMet && !helper.isMonitoringSupported()) { + EventService.retrieveFromContext(context) + .fireEvent( + nessie, + ServiceMonitorNotSupported, + "Service monitor creation requested, but monitoring is not supported"); + conditionMet = false; + } + return conditionMet; + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/Nessie.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/Nessie.java new file mode 100644 index 00000000000..fd9a1e35842 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/Nessie.java @@ -0,0 +1,53 @@ +/* + * 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.resource; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; +import io.sundr.builder.annotations.Buildable; +import io.sundr.builder.annotations.BuildableReference; +import org.projectnessie.operator.utils.ResourceUtils; + +@Version(Nessie.VERSION) +@Group(Nessie.GROUP) +@Buildable( + builderPackage = "io.fabric8.kubernetes.api.builder", + editableEnabled = false, + refs = { + @BuildableReference(ObjectMeta.class), + @BuildableReference(CustomResource.class), + }) +public class Nessie extends CustomResource implements Namespaced { + + public static final String GROUP = "nessie.projectnessie.org"; + public static final String VERSION = "v1alpha1"; + public static final String KIND = "Nessie"; + + public void validate() { + // cap at 50 characters to accommodate for suffixes like "-gc", "-mgmt", etc. + ResourceUtils.validateName(getMetadata().getName(), 50); + getSpec().validate(); + } + + @JsonIgnore + public NessieBuilder edit() { + return new NessieBuilder(this); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/NessieSpec.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/NessieSpec.java new file mode 100644 index 00000000000..6e140727a3b --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/NessieSpec.java @@ -0,0 +1,219 @@ +/* + * 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.resource; + +import static org.projectnessie.operator.events.EventReason.AutoscalingNotAllowed; +import static org.projectnessie.operator.events.EventReason.InvalidAdvancedConfig; +import static org.projectnessie.operator.events.EventReason.MultipleReplicasNotAllowed; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import io.fabric8.crd.generator.annotation.PrinterColumn; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Min; +import io.fabric8.kubernetes.api.model.EnvVar; +import io.sundr.builder.annotations.Buildable; +import java.util.List; +import java.util.Map; +import org.projectnessie.operator.exception.InvalidSpecException; +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.IngressOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.MonitoringOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.RemoteDebugOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.ServiceOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.TelemetryOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.VersionStoreOptions; +import org.projectnessie.operator.reconciler.nessie.resource.options.VersionStoreOptions.VersionStoreType; +import org.projectnessie.operator.reconciler.nessie.resource.options.WorkloadOptions; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record NessieSpec( + @JsonPropertyDescription( + "The number of replicas to run, defaults to 1. Ignored when autoscaling is enabled.") + @Default("1") + @Min(0) // allow deployments to scale down to 0 (e.g. for maintenance) + @PrinterColumn(name = "Size") + Integer size, + @JsonPropertyDescription("The log level to use for the Nessie server.") // + @Default("INFO") + LogLevel logLevel, + @JsonPropertyDescription("Nessie version store options.") // + @Default("{}") + VersionStoreOptions versionStore, + @JsonPropertyDescription("Nessie service options.") // + @Default("{}") + ServiceOptions service, + @JsonPropertyDescription("Nessie ingress options.") // + @Default("{}") + IngressOptions ingress, + @JsonPropertyDescription("Nessie authentication options.") // + @Default("{}") + AuthenticationOptions authentication, + @JsonPropertyDescription("Nessie authorization options.") // + @Default("{}") + AuthorizationOptions authorization, + @JsonPropertyDescription("Nessie telemetry options.") // + @Default("{}") + TelemetryOptions telemetry, + @JsonPropertyDescription("Nessie monitoring options.") // + @Default("{}") + MonitoringOptions monitoring, + @JsonPropertyDescription("Nessie autoscaling options.") // + @Default("{}") + AutoscalingOptions autoscaling, + @JsonPropertyDescription("Nessie remote debugging options.") // + @Default("{}") + RemoteDebugOptions remoteDebug, + @JsonPropertyDescription( + """ + Extra (advanced) configuration. \ + You can pass here any valid Nessie or Quarkus configuration property. \ + Properties defined here will override any configuration property \ + generated by this operator, with the exception of environment variables \ + defined in extraEnv, which have even higher priority. + """) + @JsonAnySetter + @Default("{}") + JsonNode advancedConfig, + @JsonPropertyDescription( + """ + Extra JVM options to add to the Nessie server container. \ + These options will be merged together and included in the \ + JAVA_OPTS_APPEND environment variable. By default, \ + the operator sets the following JVM options: \ + -XX:InitialRAMPercentage=70.0, -XX:MaxRAMPercentage=70.0. \ + This makes the JVM use 70% of the container's memory. + """) + @Default( + """ + ["-XX:InitialRAMPercentage=70.0", "-XX:MaxRAMPercentage=70.0"]""") + List jvmOptions, + @JsonPropertyDescription( + """ + Extra environment variables to add to the Nessie server container. \ + Any environment variable defined here will override any environment \ + variables defined elsewhere, or generated by this operator. + """) + @Default("[]") + List extraEnv, + @JsonPropertyDescription( + """ + Options for the Nessie deployment (service account, container image, \ + security context, etc.).""") + @Default("{}") + WorkloadOptions deployment) { + + private static final List DEFAULT_JVM_OPTIONS = + List.of("-XX:InitialRAMPercentage=70.0", "-XX:MaxRAMPercentage=70.0"); + + public enum LogLevel { + DEBUG, + INFO, + WARN, + ERROR + } + + public NessieSpec() { + this(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + } + + /** + * Compact constructor enforcing default values. + * + * @implNote most of the records in this package and child packages have a compact constructor + * enforcing default values. This is necessary because default values are only applied + * automatically server-side if a defaulting webhook is registered. This is not always the + * case, which is why we need to enforce them programmatically. This is also useful for unit + * tests. + */ + public NessieSpec { + size = size != null ? size : 1; + logLevel = logLevel != null ? logLevel : LogLevel.INFO; + versionStore = versionStore != null ? versionStore : new VersionStoreOptions(); + service = service != null ? service : new ServiceOptions(); + ingress = ingress != null ? ingress : new IngressOptions(); + authentication = authentication != null ? authentication : new AuthenticationOptions(); + authorization = authorization != null ? authorization : new AuthorizationOptions(); + telemetry = telemetry != null ? telemetry : new TelemetryOptions(); + monitoring = monitoring != null ? monitoring : new MonitoringOptions(); + autoscaling = autoscaling != null ? autoscaling : new AutoscalingOptions(); + remoteDebug = remoteDebug != null ? remoteDebug : new RemoteDebugOptions(); + advancedConfig = + advancedConfig != null ? advancedConfig : JsonNodeFactory.instance.objectNode(); + extraEnv = extraEnv != null ? List.copyOf(extraEnv) : List.of(); + jvmOptions = jvmOptions != null ? List.copyOf(jvmOptions) : DEFAULT_JVM_OPTIONS; + deployment = deployment != null ? deployment : new WorkloadOptions(); + } + + public void validate() { + versionStore.validate(); + authentication.validate(); + authorization.validate(); + ingress.validate(); + telemetry.validate(); + autoscaling.validate(); + validateReplicas(); + validateAdvancedConfig(); + } + + private void validateReplicas() { + VersionStoreType type = versionStore().type(); + if (!type.supportsMultipleReplicas()) { + if (size() > 1) { + throw new InvalidSpecException( + MultipleReplicasNotAllowed, + type + " version store can only be used with a single replica."); + } + if (autoscaling().enabled()) { + throw new InvalidSpecException( + AutoscalingNotAllowed, + "Autoscaling is not allowed with %s version store.".formatted(type)); + } + } + } + + private void validateAdvancedConfig() { + if (!advancedConfig.isObject()) { + throw new InvalidSpecException( + InvalidAdvancedConfig, + "Invalid advanced config: expected root object, got %s" + .formatted(advancedConfig.getNodeType())); + } + validateAdvancedConfig(advancedConfig, ""); + } + + private static void validateAdvancedConfig(JsonNode configNode, String prefix) { + for (Map.Entry entry : configNode.properties()) { + String key = prefix + entry.getKey(); + JsonNode value = entry.getValue(); + if (value.isObject()) { + validateAdvancedConfig(value, key + "."); + } else if (!value.isValueNode()) { + throw new InvalidSpecException( + InvalidAdvancedConfig, + "Invalid advanced config at key %s: expected object or scalar, got %s" + .formatted(key, value.getNodeType())); + } + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/NessieStatus.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/NessieStatus.java new file mode 100644 index 00000000000..ccd27cd98f2 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/NessieStatus.java @@ -0,0 +1,81 @@ +/* + * 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.resource; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import io.fabric8.crd.generator.annotation.PrinterColumn; +import io.fabric8.kubernetes.api.model.Condition; +import io.fabric8.kubernetes.api.model.ConditionBuilder; +import io.javaoperatorsdk.operator.api.ObservedGenerationAwareStatus; +import io.sundr.builder.annotations.Buildable; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import org.projectnessie.operator.utils.EventUtils; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +public class NessieStatus extends ObservedGenerationAwareStatus { + + @PrinterColumn(name = "Ready") + private boolean ready; + + @JsonInclude(Include.NON_EMPTY) + private List conditions = new ArrayList<>(); + + @JsonInclude(Include.NON_NULL) + @PrinterColumn(name = "Ingress URL", priority = 10) + private String exposedUrl; + + public boolean isReady() { + return ready; + } + + public void setReady(boolean ready) { + this.ready = ready; + setCondition( + new ConditionBuilder() + .withLastTransitionTime(EventUtils.formatTime(ZonedDateTime.now())) + .withType("Ready") + .withStatus(ready ? "True" : "False") + .withMessage(ready ? "Nessie is ready" : "Nessie is not ready") + .withReason(ready ? "NessieReady" : "NessieNotReady") + .build()); + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } + + @JsonIgnore + public void setCondition(Condition condition) { + conditions.removeIf(c -> c.getType().equals(condition.getType())); + conditions.add(condition); + } + + public String getExposedUrl() { + return exposedUrl; + } + + public void setExposedUrl(String exposedUrl) { + this.exposedUrl = exposedUrl; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AuthenticationOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AuthenticationOptions.java new file mode 100644 index 00000000000..79b4333c528 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AuthenticationOptions.java @@ -0,0 +1,78 @@ +/* + * 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.resource.options; + +import static org.projectnessie.operator.events.EventReason.InvalidAuthenticationConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.crd.generator.annotation.PrinterColumn; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.sundr.builder.annotations.Buildable; +import org.projectnessie.operator.exception.InvalidSpecException; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record AuthenticationOptions( + @JsonPropertyDescription( + "Specifies whether authentication for the nessie server should be enabled.") + @PrinterColumn(name = "AuthN", priority = 1) + Boolean enabled, + @JsonPropertyDescription( + "Sets the base URL of the OpenID Connect (OIDC) server. Required if authentication is enabled.") + @Nullable + @jakarta.annotation.Nullable + String oidcAuthServerUrl, + @JsonPropertyDescription( + "OIDC client ID to use when authentication is enabled, in order to identify the application.") + @Default("nessie") + String oidcClientId, + @JsonPropertyDescription( + """ + OIDC client secret to use when authentication is enabled. Whether the client secret \ + is required depends on the OIDC server configuration. If tokens can be introspected locally, this is usually not required. \ + If token introspection requires a round-trip to the OIDC server, then the client secret is required. + """) + @Nullable + @jakarta.annotation.Nullable + SecretValue oidcClientSecret) { + + public AuthenticationOptions() { + this(null, null, null, null); + } + + public AuthenticationOptions { + enabled = enabled != null ? enabled : false; + oidcClientId = oidcClientId != null ? oidcClientId : "nessie"; + } + + public void validate() { + if (enabled) { + if (oidcAuthServerUrl == null) { + throw new InvalidSpecException( + InvalidAuthenticationConfig, + "OIDC authentication is enabled, but no OIDC auth server URL is configured."); + } + if (oidcClientId == null) { + throw new InvalidSpecException( + InvalidAuthenticationConfig, + "OIDC authentication is enabled, but no OIDC client ID is configured."); + } + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AuthorizationOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AuthorizationOptions.java new file mode 100644 index 00000000000..50d86162aab --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AuthorizationOptions.java @@ -0,0 +1,62 @@ +/* + * 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.resource.options; + +import static org.projectnessie.operator.events.EventReason.InvalidAuthorizationConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.crd.generator.annotation.PrinterColumn; +import io.fabric8.generator.annotation.Default; +import io.sundr.builder.annotations.Buildable; +import java.util.Map; +import org.projectnessie.operator.exception.InvalidSpecException; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record AuthorizationOptions( + @JsonPropertyDescription( + "Specifies whether authorization for the Nessie server should be enabled.") + @Default("false") + @PrinterColumn(name = "AuthZ", priority = 1) + Boolean enabled, + @JsonPropertyDescription( + """ + The authorization rules when authorization.enabled=true. \ + Example rules can be found at \ + https://projectnessie.org/features/metadata_authorization/#authorization-rules""") + @Default("{}") + Map rules) { + + public AuthorizationOptions() { + this(null, null); + } + + public AuthorizationOptions { + enabled = enabled != null ? enabled : false; + rules = rules != null ? Map.copyOf(rules) : Map.of(); + } + + public void validate() { + if (enabled) + if (rules().isEmpty()) { + throw new InvalidSpecException( + InvalidAuthorizationConfig, + "Authorization is enabled, but no authorization rules are configured."); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AutoscalingOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AutoscalingOptions.java new file mode 100644 index 00000000000..04e590a8d46 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AutoscalingOptions.java @@ -0,0 +1,88 @@ +/* + * 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.resource.options; + +import static org.projectnessie.operator.events.EventReason.InvalidAutoScalingConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Min; +import io.fabric8.generator.annotation.Nullable; +import io.sundr.builder.annotations.Buildable; +import org.projectnessie.operator.exception.InvalidSpecException; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record AutoscalingOptions( + @JsonPropertyDescription( + """ + Specifies whether automatic horizontal scaling should be enabled. \ + Do not enable this when using InMemory or RocksDb version store type. + """) + @Default("false") + Boolean enabled, + @JsonPropertyDescription("The minimum number of replicas to maintain.") // + @Default("1") + @Min(1) + Integer minReplicas, + @JsonPropertyDescription("The maximum number of replicas to maintain.") // + @Default("3") + @Min(1) + Integer maxReplicas, + @JsonPropertyDescription( + "The target CPU utilization percentage. Set to zero or empty to disable.") + @Nullable + @jakarta.annotation.Nullable + Integer targetCpuUtilizationPercentage, + @JsonPropertyDescription( + "The target memory utilization percentage. Set to zero or empty to disable.") + @Nullable + @jakarta.annotation.Nullable + Integer targetMemoryUtilizationPercentage) { + + public AutoscalingOptions() { + this(null, null, null, null, null); + } + + public AutoscalingOptions { + enabled = enabled != null ? enabled : false; + minReplicas = minReplicas != null ? minReplicas : 1; + maxReplicas = maxReplicas != null ? maxReplicas : 3; + } + + public void validate() { + if (enabled) { + Integer cpu = targetCpuUtilizationPercentage(); + Integer memory = targetMemoryUtilizationPercentage(); + if (isNullOrZero(cpu) && isNullOrZero(memory)) { + throw new InvalidSpecException( + InvalidAutoScalingConfig, + "At least one of 'targetCpuUtilizationPercentage' or 'targetMemoryUtilizationPercentage' " + + "must be set when autoscaling is enabled."); + } + if (minReplicas() > maxReplicas()) { + throw new InvalidSpecException( + InvalidAutoScalingConfig, "'minReplicas' must be less than or equal to 'maxReplicas'."); + } + } + } + + private static boolean isNullOrZero(Integer i) { + return i == null || i == 0; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AwsCredentials.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AwsCredentials.java new file mode 100644 index 00000000000..d27a20ebaa0 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/AwsCredentials.java @@ -0,0 +1,44 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Required; +import io.sundr.builder.annotations.Buildable; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record AwsCredentials( + @JsonPropertyDescription( + "The name of the secret to pull the value from. The secret must be in the same namespace as the Nessie resource.") + @Required + String secret, + @JsonPropertyDescription("The secret key containing the access key id.") + @Default("access_key_id") + String accessKeyId, + @JsonPropertyDescription("The secret key containing the secret access key.") + @Default("secret_access_key") + String secretAccessKey) { + + public AwsCredentials { + secret = secret != null ? secret : "awscreds"; + accessKeyId = accessKeyId != null ? accessKeyId : "access_key_id"; + secretAccessKey = secretAccessKey != null ? secretAccessKey : "secret_access_key"; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/BigTableOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/BigTableOptions.java new file mode 100644 index 00000000000..46e7f8ba06d --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/BigTableOptions.java @@ -0,0 +1,48 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.generator.annotation.Required; +import io.sundr.builder.annotations.Buildable; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record BigTableOptions( + @JsonPropertyDescription("The Google Cloud project ID.") @Required String projectId, + @JsonPropertyDescription("The Google Cloud BigTable instance ID.") @Default("nessie-bigtable") + String instanceId, + @JsonPropertyDescription("The Google Cloud BigTable app profile ID.") @Default("default") + String appProfileId, + @JsonPropertyDescription( + """ + The BigTable credentials. When provided, the referenced secret key must contain a \ + valid JSON key that will be used for authentication. \ + If left empty, then no credentials will be mounted, which means that \ + Workload Identity will be used for authentication.""") + @Nullable + @jakarta.annotation.Nullable + SecretValue credentials) { + + public BigTableOptions { + instanceId = instanceId != null ? instanceId : "nessie-bigtable"; + appProfileId = appProfileId != null ? appProfileId : "default"; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/CassandraOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/CassandraOptions.java new file mode 100644 index 00000000000..28b4189c9c5 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/CassandraOptions.java @@ -0,0 +1,51 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.generator.annotation.Required; +import io.sundr.builder.annotations.Buildable; +import java.util.List; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record CassandraOptions( + @JsonPropertyDescription("The Cassandra keyspace to use. Defaults to \"nessie\".") + @Default("nessie") + String keyspace, + @JsonPropertyDescription("The Cassandra contact points to use (required).") // + @Required + List contactPoints, + @JsonPropertyDescription("The Cassandra local datacenter to use (required).") // + @Required + String localDatacenter, + @JsonPropertyDescription("The Cassandra username (optional).") + @Nullable + @jakarta.annotation.Nullable + String username, + @JsonPropertyDescription("The Cassandra password (optional).") + @Nullable + @jakarta.annotation.Nullable + SecretValue password) { + + public CassandraOptions { + keyspace = keyspace != null ? keyspace : "nessie"; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/DynamoDbOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/DynamoDbOptions.java new file mode 100644 index 00000000000..58b6d2591ef --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/DynamoDbOptions.java @@ -0,0 +1,32 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Required; +import io.sundr.builder.annotations.Buildable; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record DynamoDbOptions( + @JsonPropertyDescription("The AWS region to use (required).") // + @Required + String region, + @JsonPropertyDescription("The AWS credentials (required).") // + @Required + AwsCredentials credentials) {} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ImageOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ImageOptions.java new file mode 100644 index 00000000000..2d5ed6d43e6 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ImageOptions.java @@ -0,0 +1,83 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.kubernetes.api.model.LocalObjectReference; +import io.sundr.builder.annotations.Buildable; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record ImageOptions( + @JsonPropertyDescription( + """ + The image repository. Optional; if unspecified, a default repository will be selected \ + depending on the type of container being created.""") + @Nullable + @jakarta.annotation.Nullable + String repository, + @JsonPropertyDescription( + """ + The image tag to use. Defaults to "latest".""") + @Default("latest") + String tag, + @JsonPropertyDescription( + """ + The image pull policy to use. Defaults to "Always" if the tag is "latest" or \ + "latest-java", otherwise to "IfNotPresent".""") + @Nullable + @jakarta.annotation.Nullable + PullPolicy pullPolicy, + @JsonPropertyDescription( + """ + The secret to use when pulling the image from private repositories. Optional. \ + See https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod.""") + @Nullable + @jakarta.annotation.Nullable + LocalObjectReference pullSecretRef) { + + public static final String DEFAULT_NESSIE_REPOSITORY = "ghcr.io/projectnessie/nessie"; + + public ImageOptions() { + this(null, null, null, null); + } + + public ImageOptions { + tag = tag != null ? tag : "latest"; + if (pullPolicy == null) { + pullPolicy = + tag.equals("latest") || tag.equals("latest-java") + ? PullPolicy.Always + : PullPolicy.IfNotPresent; + } + } + + public enum PullPolicy { + Always, + Never, + IfNotPresent + } + + @JsonIgnore + public String fullName(String defaultRepository) { + return (repository != null ? repository : defaultRepository) + ":" + tag; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/IngressOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/IngressOptions.java new file mode 100644 index 00000000000..59b390b3703 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/IngressOptions.java @@ -0,0 +1,81 @@ +/* + * 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.resource.options; + +import static org.projectnessie.operator.events.EventReason.InvalidIngressConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.generator.annotation.Required; +import io.sundr.builder.annotations.Buildable; +import java.util.List; +import java.util.Map; +import org.projectnessie.operator.exception.InvalidSpecException; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record IngressOptions( + @JsonPropertyDescription( + "Specifies whether an ingress should be created. The default is false.") + @Default("false") + Boolean enabled, + @JsonPropertyDescription( + """ + The ingress class name to use. If not specified, the default class name is used.""") + @Nullable + @jakarta.annotation.Nullable + String ingressClassName, + @JsonPropertyDescription("Annotations to add to the ingress.") // + @Default("{}") + Map annotations, + @JsonPropertyDescription( + "A list of rules used configure the ingress. Required if ingress is enabled.") + @Default("[]") + List rules, + @JsonPropertyDescription( + """ + A list of TLS certificates; each entry has a list of hosts in the certificate, \ + along with the secret name used to terminate TLS traffic on port 443.""") + @Default("[]") + List tls) { + + public record Rule(@Required String host, @Required List paths) {} + + public record Tls(@Required List hosts, @Required String secret) {} + + public IngressOptions() { + this(null, null, null, null, null); + } + + public IngressOptions { + enabled = enabled != null ? enabled : false; + annotations = annotations != null ? Map.copyOf(annotations) : Map.of(); + rules = rules != null ? List.copyOf(rules) : List.of(); + tls = tls != null ? List.copyOf(tls) : List.of(); + } + + public void validate() { + if (enabled) { + if (rules().isEmpty()) { + throw new InvalidSpecException( + InvalidIngressConfig, "At least one Ingress 'rule' must be defined."); + } + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/JdbcOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/JdbcOptions.java new file mode 100644 index 00000000000..e5a69bb14b9 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/JdbcOptions.java @@ -0,0 +1,69 @@ +/* + * 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.resource.options; + +import static org.projectnessie.operator.events.EventReason.InvalidVersionStoreConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.generator.annotation.Required; +import io.sundr.builder.annotations.Buildable; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.projectnessie.operator.exception.InvalidSpecException; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record JdbcOptions( + @JsonPropertyDescription("The JDBC connection URL (required).") // + @Required + String url, + @JsonPropertyDescription("The JDBC username (optional).") + @Nullable + @jakarta.annotation.Nullable // + String username, + @JsonPropertyDescription("The JDBC password (optional).") + @Nullable + @jakarta.annotation.Nullable // + SecretValue password) { + + private static final Pattern JDBC_URL_PATTERN = Pattern.compile("jdbc:(\\w+):.*"); + + public record DataSource(String name) { + + public String configPrefix() { + return "quarkus.datasource." + name() + "."; + } + } + + public DataSource datasource() { + Matcher matcher = JDBC_URL_PATTERN.matcher(url()); + if (matcher.matches()) { + return new DataSource(matcher.group(1).toLowerCase(Locale.ROOT)); + } + throw new InvalidSpecException(InvalidVersionStoreConfig, "Invalid JDBC URL"); + } + + public void validate() { + if (url() == null || url().isEmpty()) { + throw new InvalidSpecException(InvalidVersionStoreConfig, "JDBC URL is required"); + } + datasource(); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/MongoDbOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/MongoDbOptions.java new file mode 100644 index 00000000000..2f5102c8402 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/MongoDbOptions.java @@ -0,0 +1,41 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.generator.annotation.Required; +import io.sundr.builder.annotations.Buildable; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record MongoDbOptions( + @JsonPropertyDescription("The MongoDB connection string (required).") // + @Required + String connectionString, + @JsonPropertyDescription("The MongoDB database name (required).") // + @Required + String database, + @JsonPropertyDescription("The MongoDB username (optional).") + @Nullable + @jakarta.annotation.Nullable + String username, + @JsonPropertyDescription("The MongoDB password (optional).") + @Nullable + @jakarta.annotation.Nullable + SecretValue password) {} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/MonitoringOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/MonitoringOptions.java new file mode 100644 index 00000000000..33e55dec514 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/MonitoringOptions.java @@ -0,0 +1,54 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.generator.annotation.Pattern; +import io.sundr.builder.annotations.Buildable; +import java.util.Map; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +public record MonitoringOptions( + @JsonPropertyDescription( + """ + Specifies whether to enable monitoring with Prometheus. \ + If enabled, then a ServiceMonitor will be created. \ + The default is true if Prometheus monitoring is available in the cluster, false otherwise.""") + @Default("true") + Boolean enabled, + @JsonPropertyDescription( + "The scrape interval; if not specified, Prometheus' global scrape interval is used. Must be a valid duration, e.g. 1d, 1h30m, 5m, 10s.") + @Pattern( + "^(0|(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?)$") + @Nullable + @jakarta.annotation.Nullable + String interval, + @JsonPropertyDescription( + "Labels for the created ServiceMonitor so that Prometheus operator can properly pick it up.") + @Default("{}") + Map labels) { + + public MonitoringOptions() { + this(true, null, null); + } + + public MonitoringOptions { + enabled = enabled != null ? enabled : true; + labels = labels != null ? Map.copyOf(labels) : Map.of(); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ProbeOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ProbeOptions.java new file mode 100644 index 00000000000..ae83a0116cd --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ProbeOptions.java @@ -0,0 +1,77 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Min; +import io.fabric8.generator.annotation.Nullable; +import io.sundr.builder.annotations.Buildable; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record ProbeOptions( + @JsonPropertyDescription( + """ + Number of seconds after the container has started before probes are initiated. \ + Defaults to 0 seconds. Minimum value is 0. + """) + @Nullable + @jakarta.annotation.Nullable + @Min(0) + Integer initialDelaySeconds, + @JsonPropertyDescription( + """ + How often (in seconds) to perform the probe. Defaults to 10 seconds. Minimum value is 1. + """) + @Nullable + @jakarta.annotation.Nullable + @Min(1) + Integer periodSeconds, + @JsonPropertyDescription( + """ + Number of seconds after which the probe times out. Defaults to 1 second. Minimum value is 1. + """) + @Nullable + @jakarta.annotation.Nullable + @Min(1) + Integer timeoutSeconds, + @JsonPropertyDescription( + """ + Minimum consecutive successes for the probe to be considered successful after having failed. \ + Defaults to 1. Minimum value is 1. + """) + @Nullable + @jakarta.annotation.Nullable + @Min(1) + Integer successThreshold, + @JsonPropertyDescription( + """ + After a probe fails failureThreshold times in a row, Kubernetes considers that the overall check has failed: \ + the container is not healthy. + """) + @Nullable + @jakarta.annotation.Nullable + @Min(1) + Integer failureThreshold) { + + public static final ProbeOptions DEFAULT_LIVENESS_PROBE_OPTIONS = + new ProbeOptions(2, 30, 10, 1, 3); + + public static final ProbeOptions DEFAULT_READINESS_PROBE_OPTIONS = + new ProbeOptions(3, 45, 10, 1, 3); +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/RemoteDebugOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/RemoteDebugOptions.java new file mode 100644 index 00000000000..5aad6c382f8 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/RemoteDebugOptions.java @@ -0,0 +1,48 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.sundr.builder.annotations.Buildable; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record RemoteDebugOptions( + @JsonPropertyDescription("Whether to enable remote debugging.") // + @Default("false") + Boolean enabled, + @JsonPropertyDescription("The port to use for remote debugging.") // + @Default("5005") + Integer port, + @JsonPropertyDescription("Whether to suspend.") // + @Default("false") + Boolean suspend) { + + public static final int DEFAULT_DEBUG_PORT = 5005; + + public RemoteDebugOptions() { + this(null, null, null); + } + + public RemoteDebugOptions { + enabled = enabled != null ? enabled : false; + port = port != null ? port : DEFAULT_DEBUG_PORT; + suspend = suspend != null ? suspend : false; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/RocksDbOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/RocksDbOptions.java new file mode 100644 index 00000000000..a36ae0c04c0 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/RocksDbOptions.java @@ -0,0 +1,50 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.kubernetes.api.model.Quantity; +import io.sundr.builder.annotations.Buildable; +import java.util.Map; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record RocksDbOptions( + @JsonPropertyDescription( + "The storage class name of the persistent volume claim to create. Leave unset if using dynamic provisioning.") + @Nullable + @jakarta.annotation.Nullable + String storageClassName, + @JsonPropertyDescription("The size of the persistent volume claim to create.") // + @Default("1Gi") + Quantity storageSize, + @JsonPropertyDescription( + """ + Labels to add to the persistent volume claim spec selector; \ + a persistent volume with matching labels must exist. \ + Leave empty if using dynamic provisioning.""") + @Default("{}") + Map selectorLabels) { + + public RocksDbOptions { + storageSize = storageSize != null ? storageSize : new Quantity("1Gi"); + selectorLabels = selectorLabels != null ? Map.copyOf(selectorLabels) : Map.of(); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/SecretValue.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/SecretValue.java new file mode 100644 index 00000000000..38341a7f960 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/SecretValue.java @@ -0,0 +1,33 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Required; +import io.sundr.builder.annotations.Buildable; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +public record SecretValue( + @JsonPropertyDescription( + """ + The name of the secret to pull the value from. \ + The secret must be in the same namespace as the Nessie resource. + """) + @Required + String secret, + @JsonPropertyDescription("The secret key to read the value from.") // + @Required + String key) {} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ServiceAccountOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ServiceAccountOptions.java new file mode 100644 index 00000000000..d29ca955c38 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ServiceAccountOptions.java @@ -0,0 +1,53 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.sundr.builder.annotations.Buildable; +import java.util.Map; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record ServiceAccountOptions( + @JsonPropertyDescription("Specifies whether a service account should be created.") + @Default("false") + Boolean create, + @JsonPropertyDescription( + """ + The name of the service account to use. \ + If not set and create is true, the account will be named after the resource's name; \ + if not set and create is false, the account will be 'default'.""") + @Nullable + @jakarta.annotation.Nullable + String name, + @JsonPropertyDescription( + "Annotations to add to the service account. Only relevant if create is true, ignored otherwise.") + @Default("{}") + Map annotations) { + + public ServiceAccountOptions() { + this(null, null, null); + } + + public ServiceAccountOptions { + create = create != null ? create : false; + annotations = annotations != null ? Map.copyOf(annotations) : Map.of(); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ServiceOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ServiceOptions.java new file mode 100644 index 00000000000..269663d43c3 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/ServiceOptions.java @@ -0,0 +1,80 @@ +/* + * 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.resource.options; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.sundr.builder.annotations.Buildable; +import java.util.Map; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record ServiceOptions( + @JsonPropertyDescription("The type of service to create. Defaults to ClusterIP.") + @Default("ClusterIP") + ServiceOptions.Type type, + @JsonPropertyDescription( + "The port on which the service should listen. Defaults to " + DEFAULT_NESSIE_PORT + ".") + @Default("19120") + Integer port, + @JsonPropertyDescription( + """ + The node port on which the service should be exposed. \ + Only valid if the service type is NodePort or LoadBalancer, ignored otherwise. \ + If unspecified, a random node port will be assigned.""") + @Nullable + @jakarta.annotation.Nullable + Integer nodePort, + @JsonPropertyDescription("The session affinity to use for the service. Defaults to None.") + @Default("None") + SessionAffinity sessionAffinity, + @JsonPropertyDescription("Additional service labels.") // + @Default("{}") + Map labels, + @JsonPropertyDescription("Additional service annotations.") // + @Default("{}") + Map annotations) { + + public static final int DEFAULT_NESSIE_PORT = 19120; + + public enum Type { + ClusterIP, + NodePort, + LoadBalancer + } + + public enum SessionAffinity { + @JsonPropertyDescription("None disables session affinity.") + None, + @JsonPropertyDescription("ClientIP enables session affinity based on the client's IP address.") + ClientIP + } + + public ServiceOptions() { + this(null, null, null, null, null, null); + } + + public ServiceOptions { + type = type != null ? type : Type.ClusterIP; + port = port != null ? port : DEFAULT_NESSIE_PORT; + sessionAffinity = sessionAffinity != null ? sessionAffinity : SessionAffinity.None; + labels = labels != null ? Map.copyOf(labels) : Map.of(); + annotations = annotations != null ? Map.copyOf(annotations) : Map.of(); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/TelemetryOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/TelemetryOptions.java new file mode 100644 index 00000000000..bd4dc1c03e9 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/TelemetryOptions.java @@ -0,0 +1,75 @@ +/* + * 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.resource.options; + +import static org.projectnessie.operator.events.EventReason.InvalidTelemetryConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.crd.generator.annotation.PrinterColumn; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.sundr.builder.annotations.Buildable; +import java.util.Map; +import org.projectnessie.operator.exception.InvalidSpecException; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record TelemetryOptions( + @JsonPropertyDescription("Specifies whether tracing for the nessie server should be enabled.") + @Default("false") + @PrinterColumn(name = "Telemetry", priority = 1) + Boolean enabled, + @JsonPropertyDescription( + "The collector endpoint URL to connect to. Required if telemetry is enabled.") + @Nullable + @jakarta.annotation.Nullable + String endpoint, + @JsonPropertyDescription( + """ + Which requests should be sampled. Valid values are: "all", "none", or a ratio between 0.0 and \ + "1.0d" (inclusive). E.g. "0.5d" means that 50% of the requests will be sampled.""") + @Default("1.0d") + String sample, + @JsonPropertyDescription( + """ + Resource attributes to identify the nessie service among other tracing sources. \ + See https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/#service. \ + If left empty, traces will be attached to a service named after the Nessie CRD name; \ + to change this, provide a service.name attribute here.""") + @Default("{}") + Map attributes) { + + public TelemetryOptions() { + this(null, null, null, null); + } + + public TelemetryOptions { + enabled = enabled != null ? enabled : false; + sample = sample != null ? sample : "1.0d"; + attributes = attributes != null ? Map.copyOf(attributes) : Map.of(); + } + + public void validate() { + if (enabled) + if (endpoint == null) { + throw new InvalidSpecException( + InvalidTelemetryConfig, + "Telemetry is enabled, but no telemetry endpoint is configured."); + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/VersionStoreCacheOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/VersionStoreCacheOptions.java new file mode 100644 index 00000000000..b4e83153b28 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/VersionStoreCacheOptions.java @@ -0,0 +1,103 @@ +/* + * 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.resource.options; + +import static org.projectnessie.operator.events.EventReason.InvalidVersionStoreConfig; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.kubernetes.api.model.Quantity; +import io.sundr.builder.annotations.Buildable; +import java.math.BigDecimal; +import org.projectnessie.operator.exception.InvalidSpecException; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record VersionStoreCacheOptions( + @JsonPropertyDescription("Whether to enable the version store cache. The default is true.") + @Default("true") + Boolean enabled, + @JsonPropertyDescription( + "A fixed size for the cache. If this option is defined, other cache options are ignored.") + @Nullable + @jakarta.annotation.Nullable + Quantity fixedSize, + @JsonPropertyDescription( + """ + The fraction of the available heap that the cache should use. \ + The default is 700m (70%). Must be > 0 and < 1000m. \ + Note: by default, Nessie servers are configured to use 70% of the available memory, \ + so the cache will by default use 70% of that.""") + @Default("700m") + Quantity heapFraction, + @JsonPropertyDescription( + """ + The minimum size of the cache. \ + This serves as a lower bound for the cache size. \ + The default is 64Mi. Cannot be less than 64Mi.""") + @Default("64Mi") + Quantity minSize, + @JsonPropertyDescription( + """ + The minimum amount of heap that should be kept free. \ + This servers as an upper bound for the cache size. \ + The default is 256Mi. Cannot be less than 64Mi.""") + @Default("256Mi") + Quantity minFreeHeap) { + + // These constants should be kept in sync + // with org.projectnessie.versioned.storage.cache.CacheSizing + + public static final Quantity DEFAULT_HEAP_PERCENTAGE = Quantity.parse("700m"); + public static final Quantity DEFAULT_MIN_SIZE = Quantity.parse("64Mi"); + public static final Quantity DEFAULT_MIN_FREE_HEAP = Quantity.parse("256Mi"); + public static final Quantity MIN_SIZE = Quantity.parse("64Mi"); + + public VersionStoreCacheOptions() { + this(null, null, null, null, null); + } + + public VersionStoreCacheOptions { + enabled = enabled != null ? enabled : true; + heapFraction = heapFraction != null ? heapFraction : DEFAULT_HEAP_PERCENTAGE; + minSize = minSize != null ? minSize : DEFAULT_MIN_SIZE; + minFreeHeap = minFreeHeap != null ? minFreeHeap : DEFAULT_MIN_FREE_HEAP; + } + + public void validate() { + if (enabled) { + if (heapFraction.getNumericalAmount().compareTo(BigDecimal.ZERO) <= 0 + || heapFraction.getNumericalAmount().compareTo(BigDecimal.ONE) >= 0) { + throw new InvalidSpecException( + InvalidVersionStoreConfig, + "Invalid cache configuration: spec.versionStore.cache.heapFraction must be > 0 and < 1"); + } + if (minSize.getNumericalAmount().compareTo(MIN_SIZE.getNumericalAmount()) < 0) { + throw new InvalidSpecException( + InvalidVersionStoreConfig, + "Invalid cache configuration: spec.versionStore.cache.minSize must be >= 64Mi"); + } + if (minFreeHeap.getNumericalAmount().compareTo(MIN_SIZE.getNumericalAmount()) < 0) { + throw new InvalidSpecException( + InvalidVersionStoreConfig, + "Invalid cache configuration: spec.versionStore.cache.minFreeHeap must be >= 64Mi"); + } + } + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/VersionStoreOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/VersionStoreOptions.java new file mode 100644 index 00000000000..c76ecccd3d6 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/VersionStoreOptions.java @@ -0,0 +1,132 @@ +/* + * 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.resource.options; + +import static org.apache.commons.lang3.StringUtils.uncapitalize; +import static org.projectnessie.operator.events.EventReason.InvalidVersionStoreConfig; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.crd.generator.annotation.PrinterColumn; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.sundr.builder.annotations.Buildable; +import org.projectnessie.operator.exception.InvalidSpecException; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record VersionStoreOptions( + @JsonPropertyDescription("The type of version store to use.") + @Default("InMemory") + @PrinterColumn(name = "Version Store") + VersionStoreType type, + @JsonPropertyDescription("Version store cache options.") @Default("{}") + VersionStoreCacheOptions cache, + @JsonPropertyDescription( + "RocksDB options. Only required when using RocksDb version store type; must be null otherwise.") + @Nullable + @jakarta.annotation.Nullable + RocksDbOptions rocksDb, + @JsonPropertyDescription( + "DynamoDB options. Only required when using DynamoDb version store type; must be null otherwise.") + @Nullable + @jakarta.annotation.Nullable + DynamoDbOptions dynamoDb, + @JsonPropertyDescription( + "MongoDB options. Only required when using MongoDb version store type; must be null otherwise.") + @Nullable + @jakarta.annotation.Nullable + MongoDbOptions mongoDb, + @JsonPropertyDescription( + "Cassandra options. Only required when using Cassandra version store type; must be null otherwise.") + @Nullable + @jakarta.annotation.Nullable + CassandraOptions cassandra, + @JsonPropertyDescription( + "JDBC options. Only required when using Jdbc version store type; must be null otherwise.") + @Nullable + @jakarta.annotation.Nullable + JdbcOptions jdbc, + @JsonPropertyDescription( + "BigTable options. Only required when using BigTable version store type; must be null otherwise.") + @Nullable + @jakarta.annotation.Nullable + BigTableOptions bigTable) { + + public enum VersionStoreType { + InMemory, + RocksDb, + DynamoDb, + MongoDb, + Cassandra, + Jdbc, + BigTable; + + @JsonIgnore + public boolean supportsMultipleReplicas() { + return this != InMemory && this != RocksDb; + } + + @JsonIgnore + public boolean requiresPvc() { + return this == VersionStoreType.RocksDb; + } + } + + public VersionStoreOptions() { + this(null, null, null, null, null, null, null, null); + } + + public VersionStoreOptions { + type = type != null ? type : VersionStoreType.InMemory; + cache = cache != null ? cache : new VersionStoreCacheOptions(); + } + + public void validate() { + for (VersionStoreType vst : VersionStoreType.values()) { + if (vst != VersionStoreType.InMemory && vst == type && !isConfigured(vst)) { + throw new InvalidSpecException( + InvalidVersionStoreConfig, + "Version store type is '%s', but spec.versionStore.%s is not configured." + .formatted(type, uncapitalize(vst.name()))); + } + if (vst != type && isConfigured(vst)) { + throw new InvalidSpecException( + InvalidVersionStoreConfig, + "Version store type is '%s', but spec.versionStore.%s is configured." + .formatted(type, uncapitalize(vst.name()))); + } + } + cache.validate(); + if (jdbc != null) { + jdbc.validate(); + } + } + + private boolean isConfigured(VersionStoreType type) { + return switch (type) { + case InMemory -> false; + case RocksDb -> rocksDb != null; + case DynamoDb -> dynamoDb != null; + case MongoDb -> mongoDb != null; + case Cassandra -> cassandra != null; + case Jdbc -> jdbc != null; + case BigTable -> bigTable != null; + }; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/WorkloadOptions.java b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/WorkloadOptions.java new file mode 100644 index 00000000000..eb8fdc4b053 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/reconciler/nessie/resource/options/WorkloadOptions.java @@ -0,0 +1,118 @@ +/* + * 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.resource.options; + +import static org.projectnessie.operator.reconciler.nessie.resource.options.ProbeOptions.DEFAULT_LIVENESS_PROBE_OPTIONS; +import static org.projectnessie.operator.reconciler.nessie.resource.options.ProbeOptions.DEFAULT_READINESS_PROBE_OPTIONS; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import io.fabric8.generator.annotation.Default; +import io.fabric8.generator.annotation.Nullable; +import io.fabric8.kubernetes.api.model.Affinity; +import io.fabric8.kubernetes.api.model.PodSecurityContext; +import io.fabric8.kubernetes.api.model.ResourceRequirements; +import io.fabric8.kubernetes.api.model.SecurityContext; +import io.fabric8.kubernetes.api.model.Toleration; +import io.sundr.builder.annotations.Buildable; +import java.util.List; +import java.util.Map; + +@Buildable(builderPackage = "io.fabric8.kubernetes.api.builder", editableEnabled = false) +@JsonInclude(Include.NON_NULL) +public record WorkloadOptions( + @JsonPropertyDescription("The image to use for the main container.") @Default("{}") + ImageOptions image, + @JsonPropertyDescription("Service account options.") @Default("{}") + ServiceAccountOptions serviceAccount, + @JsonPropertyDescription( + """ + The resources to allocate to the main container. \ + Note: by default, Nessie servers are configured to use 70% of the available memory.""") + @Nullable + @jakarta.annotation.Nullable + ResourceRequirements resources, + @JsonPropertyDescription( + """ + The liveness probe options for the main container. \ + See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/.""") + @Default( + """ + { "initialDelaySeconds": 2, "periodSeconds": 30, "timeoutSeconds": 10, "successThreshold": 1, "failureThreshold": 3}""") + ProbeOptions livenessProbe, + @JsonPropertyDescription( + """ + The readiness probe options for the main container. \ + See https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/.""") + @Default( + """ + { "initialDelaySeconds": 3, "periodSeconds": 45, "timeoutSeconds": 10, "successThreshold": 1, "failureThreshold": 3}""") + ProbeOptions readinessProbe, + @JsonPropertyDescription( + """ + Node labels which must match for the pod to be scheduled on that node. \ + See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodeselector.""") + @Default("{}") + Map nodeSelector, + @JsonPropertyDescription( + """ + Tolerations for the pod. \ + See https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/.""") + @Default("[]") + List tolerations, + @JsonPropertyDescription( + """ + Affinity rules for the pod. \ + See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity.""") + @Default("{}") + Affinity affinity, + @JsonPropertyDescription("Additional pod labels.") @Default("{}") Map labels, + @JsonPropertyDescription("Additional pod annotations.") @Default("{}") + Map annotations, + @JsonPropertyDescription( + """ + Security context for the pod. \ + See https://kubernetes.io/docs/tasks/configure-pod-container/security-context/.""") + @Default("{}") + PodSecurityContext podSecurityContext, + @JsonPropertyDescription( + """ + Security context for the container. \ + See https://kubernetes.io/docs/tasks/configure-pod-container/security-context/.""") + @Default("{}") + SecurityContext containerSecurityContext) { + + public WorkloadOptions() { + this(null, null, null, null, null, null, null, null, null, null, null, null); + } + + public WorkloadOptions { + image = image != null ? image : new ImageOptions(); + serviceAccount = serviceAccount != null ? serviceAccount : new ServiceAccountOptions(); + resources = resources != null ? resources : new ResourceRequirements(); + livenessProbe = livenessProbe != null ? livenessProbe : DEFAULT_LIVENESS_PROBE_OPTIONS; + readinessProbe = readinessProbe != null ? readinessProbe : DEFAULT_READINESS_PROBE_OPTIONS; + nodeSelector = nodeSelector != null ? Map.copyOf(nodeSelector) : Map.of(); + tolerations = tolerations != null ? tolerations : List.of(); + affinity = affinity != null ? affinity : new Affinity(); + labels = labels != null ? Map.copyOf(labels) : Map.of(); + annotations = annotations != null ? Map.copyOf(annotations) : Map.of(); + podSecurityContext = podSecurityContext != null ? podSecurityContext : new PodSecurityContext(); + containerSecurityContext = + containerSecurityContext != null ? containerSecurityContext : new SecurityContext(); + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/utils/EventUtils.java b/operator/src/main/java/org/projectnessie/operator/utils/EventUtils.java new file mode 100644 index 00000000000..d7c7a5c53a1 --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/utils/EventUtils.java @@ -0,0 +1,119 @@ +/* + * 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.utils; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import org.projectnessie.operator.events.EventReason; +import org.projectnessie.operator.exception.InvalidSpecException; + +public final class EventUtils { + + private EventUtils() {} + + /** + * A formatter that is compliant with the Kubernetes API server's expectations for the Time v1 + * type. + * + *

Kubernetes expects Time to be formatted as RFC 3339 with a time zone offset, or 'Z'. The Go + * constant definition is: + * + *

+   *   const RFC3339 = "2006-01-02T15:04:05Z07:00"
+   * 
+ * + * @see Time + * v1 + * @see Go time package constants + * @see Kubernetes + * time.go + */ + private static final DateTimeFormatter TIME = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); + + /** + * A formatter that is compliant with the Kubernetes API server's expectations for the MicroTime + * v1 type. MicroTime is a version of Time with microsecond-level precision. + * + *

Kubernetes expects MicroTime to be formatted as RFC 3339 with a fractional seconds part and + * a time zone offset, or 'Z'. The Go constant definition is: + * + *

+   *   const RFC3339Micro = "2006-01-02T15:04:05.000000Z07:00"
+   * 
+ * + * @see MicroTime + * v1 + * @see Go time package constants + * @see Kubernetes + * micro_time.go + */ + private static final DateTimeFormatter MICRO_TIME = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXX"); + + public static String formatTime(ZonedDateTime zdt) { + return TIME.format(zdt); + } + + public static String formatMicroTime(ZonedDateTime zdt) { + return MICRO_TIME.format(zdt); + } + + public static String eventName(HasMetadata primary, EventReason reason) { + return primary.getSingular() + "-" + primary.getMetadata().getUid() + "-" + reason; + } + + public static EventReason reasonFromEventName(String eventName) { + int lastDash = eventName.lastIndexOf('-'); + return EventReason.valueOf(eventName.substring(lastDash + 1)); + } + + public static EventReason errorReason(Throwable t) { + return t instanceof InvalidSpecException ise ? ise.getReason() : EventReason.ReconcileError; + } + + public static String formatMessage(String message, Object... args) { + // Message is limited to 1024 characters in practice + message = String.format(message, args); + if (message.length() > 1024) { + // add ellipsis to indicate that the message was truncated + String ellipsis = "... [truncated]"; + message = message.substring(0, 1024 - ellipsis.length()) + ellipsis; + } + return message; + } + + public static String getErrorMessage(Throwable t) { + return t.getMessage() == null ? t.toString() : t.getMessage(); + } + + public static Throwable launderThrowable( + Throwable t, Class preferredThrowableClass) { + Throwable t1 = t; + do { + if (preferredThrowableClass.isInstance(t1)) { + return t1; + } + t1 = t1.getCause(); + } while (t1 != null); + return t; + } +} diff --git a/operator/src/main/java/org/projectnessie/operator/utils/ResourceUtils.java b/operator/src/main/java/org/projectnessie/operator/utils/ResourceUtils.java new file mode 100644 index 00000000000..6b6279aeffc --- /dev/null +++ b/operator/src/main/java/org/projectnessie/operator/utils/ResourceUtils.java @@ -0,0 +1,56 @@ +/* + * 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.utils; + +import org.projectnessie.operator.events.EventReason; +import org.projectnessie.operator.exception.InvalidSpecException; + +public final class ResourceUtils { + + private static final int MAX_DNS_LABEL_LENGTH = 63; + + private ResourceUtils() {} + + public static void validateName(String name) { + validateName(name, MAX_DNS_LABEL_LENGTH); + } + + /** + * Validates that the given name is a valid DNS label according to RFC 1035 (which is more + * restrictive than RFC 1123). + * + * @param name the name to validate + * @param maxLength the maximum length of the name, which is 63 by default + */ + public static void validateName(String name, int maxLength) { + if (name == null || name.isEmpty()) { + throw new InvalidSpecException( + EventReason.InvalidName, "Resource name cannot be null or empty"); + } + if (name.length() > maxLength) { + throw new InvalidSpecException( + EventReason.InvalidName, + "Resource name cannot be longer than " + maxLength + " characters"); + } + if (!name.matches("[a-z]([-a-z0-9]*[a-z0-9])?")) { + throw new InvalidSpecException( + EventReason.InvalidName, + "Resource name must consist of lower case alphanumeric characters or '-', " + + "start with an alphabetic character, " + + "and end with an alphanumeric character"); + } + } +} diff --git a/operator/src/main/kubernetes/nessie.svg b/operator/src/main/kubernetes/nessie.svg new file mode 100644 index 00000000000..d3091b3ce78 --- /dev/null +++ b/operator/src/main/kubernetes/nessie.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/operator/src/main/resources/application.properties b/operator/src/main/resources/application.properties new file mode 100644 index 00000000000..90257e19f72 --- /dev/null +++ b/operator/src/main/resources/application.properties @@ -0,0 +1,61 @@ +# Application settings +# Quarkus settings +# Visit here for all configs: https://quarkus.io/guides/all-config +# some parameters are only configured at build time. See: +# https://quarkus.io/guides/config#overriding-properties-at-runtime + +quarkus.application.name=nessie-operator +quarkus.banner.path=nessie-banner.txt + +# Kubernetes manifests +quarkus.kubernetes.version=${nessie.version} +quarkus.kubernetes.namespace=nessie-operator +quarkus.kubernetes.image-pull-policy=IfNotPresent +quarkus.kubernetes.prometheus.generate-service-monitor=true +quarkus.kubernetes.prometheus.annotations=true +# Workaround for https://github.com/quarkusio/quarkus/issues/40369 +quarkus.kubernetes.vcs-uri.enabled=false + +# Quarkus Operator SDK settings +quarkus.operator-sdk.enable-ssa=true +quarkus.operator-sdk.crd.generate=true +quarkus.operator-sdk.crd.apply=true +quarkus.operator-sdk.helm.enabled=true +quarkus.operator-sdk.bundle.enabled=true +# https://olm.operatorframework.io/docs/best-practices/channel-naming/#channels +quarkus.operator-sdk.bundle.channels=alpha + +# Logging +# Available MDC keys: Corresponding Kubernetes resource field: +# resource.apiVersion .apiVersion +# resource.kind .kind +# resource.name .metadata.name +# resource.namespace .metadata.namespace +# resource.resourceVersion .metadata.resourceVersion +# resource.generation .metadata.generation +# resource.uid .metadata.uid +quarkus.log.level=INFO +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%X{resource.namespace} %X{resource.kind} %X{resource.name}] [%c{3.}] (%t) %s%e%n +quarkus.log.file.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%X{resource.namespace} %X{resource.kind} %X{resource.name}] [%c{3.}] (%t) %s%e%n +quarkus.log.category."io.fabric8.kubernetes".level=INFO +quarkus.log.category."io.javaoperatorsdk.operator".level=INFO +quarkus.log.category."io.quarkiverse.operatorsdk".level=INFO +quarkus.log.category."io.quarkus.kubernetes".level=INFO +quarkus.log.category."org.projectnessie".level=INFO + +# Testing + +%test.quarkus.devservices.enabled=false +%test.quarkus.kubernetes-client.devservices.enabled=false + +%test.quarkus.operator-sdk.start-operator=true +%test.quarkus.operator-sdk.close-client-on-stop=true + +%test.quarkus.log.category."okhttp3.mockwebserver".level=WARN +%test.quarkus.log.category."io.quarkus.test.kubernetes".level=INFO +%test.quarkus.log.category."io.fabric8.kubernetes.client.dsl.internal.VersionUsageUtils".level=ERROR +%test.quarkus.log.category."io.javaoperatorsdk.operator.processing.event.EventProcessor".level=OFF +%test.quarkus.log.category."org.projectnessie.operator.testinfra".level=WARN +%test.quarkus.http.test-port=0 + +#%test.quarkus.test.arg-line=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 diff --git a/operator/src/main/resources/nessie-banner.txt b/operator/src/main/resources/nessie-banner.txt new file mode 100644 index 00000000000..4f6ef4947ed --- /dev/null +++ b/operator/src/main/resources/nessie-banner.txt @@ -0,0 +1,8 @@ + _ _ _ ____ _ +| \ | | (_) / __ \ | | +| \| | ___ ___ ___ _ ___ | | | |_ __ ___ _ __ __ _| |_ ___ _ __ +| . ` |/ _ \/ __/ __| |/ _ \ | | | | '_ \ / _ \ '__/ _` | __/ _ \| '__| +| |\ | __/\__ \__ \ | __/ | |__| | |_) | __/ | | (_| | || (_) | | +|_| \_|\___||___/___/_|\___| \____/| .__/ \___|_| \__,_|\__\___/|_| + | | + |_| https://projectnessie.org/ diff --git a/operator/src/test/java/org/projectnessie/operator/reconciler/AbstractReconcilerUnitTests.java b/operator/src/test/java/org/projectnessie/operator/reconciler/AbstractReconcilerUnitTests.java new file mode 100644 index 00000000000..a54fb925517 --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/reconciler/AbstractReconcilerUnitTests.java @@ -0,0 +1,176 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.fabric8.kubernetes.api.model.APIGroupBuilder; +import io.fabric8.kubernetes.api.model.GroupVersionForDiscovery; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.autoscaling.v2.HorizontalPodAutoscaler; +import io.fabric8.kubernetes.api.model.networking.v1.Ingress; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.fabric8.openshift.client.OpenShiftClient; +import io.quarkus.test.junit.QuarkusTestProfile; +import jakarta.inject.Inject; +import java.time.Duration; +import java.util.Map; +import java.util.function.Consumer; + +public abstract class AbstractReconcilerUnitTests + extends AbstractReconcilerTests { + + @Inject + void setClient(OpenShiftClient client) { + this.client = client; + } + + @Override + protected Duration pollInterval() { + return Duration.ofMillis(100); + } + + @Override + protected Duration timeout() { + return Duration.ofSeconds(30); + } + + @Override + protected void setUpFunctionalTest() { + // No functional tests possible in unit tests, the Nessie deployment is not running + } + + @Override + protected void functionalTest() { + // No functional tests possible in unit tests, the Nessie deployment is not running + } + + @Override + protected void assertResourcesDeleted() { + // Garbage collection of dependent resources is not implemented in MockKubernetesServer, + // so we can't test that dependent resources are garbage-collected; see + // https://github.com/fabric8io/kubernetes-client/issues/5607 + assertThat(client.resource(primary).get()).isNull(); + } + + @Override + protected void checkPvc(PersistentVolumeClaim expected, PersistentVolumeClaim actual) { + super.checkPvc(expected, actual); + if (actual.getStatus() == null) { + actual.setStatus(expected.getStatus()); + client.persistentVolumeClaims().resource(actual).patch(); + } + } + + @Override + protected void checkDeployment(Deployment expected, Deployment actual) { + super.checkDeployment(expected, actual); + if (actual.getStatus() == null) { + actual.setStatus(expected.getStatus()); + client.apps().deployments().resource(actual).patchStatus(); + } + } + + @Override + protected void checkService(Service expected, Service actual) { + super.checkService(expected, actual); + if (actual.getStatus() == null) { + actual.setStatus(expected.getStatus()); + client.services().resource(actual).patch(); + } + } + + @Override + protected void checkIngress(Ingress expected, Ingress actual) { + super.checkIngress(expected, actual); + if (actual.getStatus() == null) { + actual.setStatus(expected.getStatus()); + client.network().v1().ingresses().resource(actual).patch(); + } + } + + @Override + protected void checkIngress( + io.fabric8.kubernetes.api.model.networking.v1beta1.Ingress expected, + io.fabric8.kubernetes.api.model.networking.v1beta1.Ingress actual) { + super.checkIngress(expected, actual); + if (actual.getStatus() == null) { + actual.setStatus(expected.getStatus()); + client.network().v1beta1().ingresses().resource(actual).patch(); + } + } + + @Override + protected void checkAutoscaler(HorizontalPodAutoscaler expected, HorizontalPodAutoscaler actual) { + super.checkAutoscaler(expected, actual); + if (actual.getStatus() == null) { + actual.setStatus(expected.getStatus()); + client.autoscaling().v2().horizontalPodAutoscalers().resource(actual).patchStatus(); + } + } + + @Override + protected void checkAutoscaler( + io.fabric8.kubernetes.api.model.autoscaling.v2beta2.HorizontalPodAutoscaler expected, + io.fabric8.kubernetes.api.model.autoscaling.v2beta2.HorizontalPodAutoscaler actual) { + super.checkAutoscaler(expected, actual); + if (actual.getStatus() == null) { + actual.setStatus(expected.getStatus()); + client.autoscaling().v2beta2().horizontalPodAutoscalers().resource(actual).patchStatus(); + } + } + + protected void checkAutoscaler( + io.fabric8.kubernetes.api.model.autoscaling.v2beta1.HorizontalPodAutoscaler expected, + io.fabric8.kubernetes.api.model.autoscaling.v2beta1.HorizontalPodAutoscaler actual) { + super.checkAutoscaler(expected, actual); + if (actual.getStatus() == null) { + actual.setStatus(expected.getStatus()); + client.autoscaling().v2beta1().horizontalPodAutoscalers().resource(actual).patchStatus(); + } + } + + public static class Profile implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + // Disable SSA for tests with MockKubernetesServer, see + // https://github.com/fabric8io/kubernetes-client/issues/5337 + return Map.of("quarkus.operator-sdk.enable-ssa", "false"); + } + } + + public abstract static class Setup implements Consumer { + + protected void reportApiSupported(KubernetesServer server, String group, String version) { + server + .expect() + .get() + .withPath("/apis/" + group) + .andReturn( + 200, + new APIGroupBuilder() + .withApiVersion(version) + .withKind("APIGroup") + .withVersions(new GroupVersionForDiscovery(group + "/" + version, version)) + .build()) + .always(); + } + } +} diff --git a/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerBigTable.java b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerBigTable.java new file mode 100644 index 00000000000..7059c7b55ce --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerBigTable.java @@ -0,0 +1,92 @@ +/* + * 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; + +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingHPA; +import static org.projectnessie.operator.events.EventReason.CreatingIngress; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceMonitor; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; +import org.projectnessie.operator.reconciler.AbstractReconcilerUnitTests; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; + +@QuarkusTest +@TestProfile(AbstractReconcilerUnitTests.Profile.class) +@WithKubernetesTestServer(setup = TestNessieReconcilerBigTable.Setup.class) +class TestNessieReconcilerBigTable extends AbstractReconcilerUnitTests { + + private static final String PREFIX = "/org/projectnessie/operator/tests/fixtures/bigtable/"; + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml"); + } + + @Override + protected void assertResourcesCreated() { + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test")); + checkDeployment( + load(client.apps().deployments(), PREFIX + "deployment.yaml"), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkIngress( + load(client.network().v1().ingresses(), PREFIX + "ingress.yaml"), + get(client.network().v1().ingresses(), "nessie-test")); + checkServiceMonitor( + load(client.monitoring().serviceMonitors(), PREFIX + "service-monitor.yaml"), + get(client.monitoring().serviceMonitors(), "nessie-test")); + checkAutoscaler( + load(client.autoscaling().v2beta1().horizontalPodAutoscalers(), PREFIX + "autoscaler.yaml"), + get(client.autoscaling().v2beta1().horizontalPodAutoscalers(), "nessie-test")); + checkEvents( + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + CreatingIngress, + CreatingServiceMonitor, + CreatingHPA, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.serviceAccounts()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + } + + public static class Setup extends AbstractReconcilerUnitTests.Setup { + @Override + public void accept(KubernetesServer server) { + reportApiSupported(server, "networking.k8s.io", "v1"); + reportApiSupported(server, "autoscaling", "v2beta1"); + reportApiSupported(server, "monitoring.coreos.com", "v1"); + } + } +} diff --git a/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerCassandra.java b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerCassandra.java new file mode 100644 index 00000000000..05411445aea --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerCassandra.java @@ -0,0 +1,92 @@ +/* + * 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; + +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingHPA; +import static org.projectnessie.operator.events.EventReason.CreatingIngress; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceMonitor; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; +import org.projectnessie.operator.reconciler.AbstractReconcilerUnitTests; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; + +@QuarkusTest +@TestProfile(AbstractReconcilerUnitTests.Profile.class) +@WithKubernetesTestServer(setup = TestNessieReconcilerCassandra.Setup.class) +class TestNessieReconcilerCassandra extends AbstractReconcilerUnitTests { + + private static final String PREFIX = "/org/projectnessie/operator/tests/fixtures/cassandra/"; + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml"); + } + + @Override + protected void assertResourcesCreated() { + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test")); + checkDeployment( + load(client.apps().deployments(), PREFIX + "deployment.yaml"), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkIngress( + load(client.network().v1().ingresses(), PREFIX + "ingress.yaml"), + get(client.network().v1().ingresses(), "nessie-test")); + checkServiceMonitor( + load(client.monitoring().serviceMonitors(), PREFIX + "service-monitor.yaml"), + get(client.monitoring().serviceMonitors(), "nessie-test")); + checkAutoscaler( + load(client.autoscaling().v2beta1().horizontalPodAutoscalers(), PREFIX + "autoscaler.yaml"), + get(client.autoscaling().v2beta1().horizontalPodAutoscalers(), "nessie-test")); + checkEvents( + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + CreatingIngress, + CreatingServiceMonitor, + CreatingHPA, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.serviceAccounts()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + } + + public static class Setup extends AbstractReconcilerUnitTests.Setup { + @Override + public void accept(KubernetesServer server) { + reportApiSupported(server, "networking.k8s.io", "v1"); + reportApiSupported(server, "autoscaling", "v2beta1"); + reportApiSupported(server, "monitoring.coreos.com", "v1"); + } + } +} diff --git a/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerInMemory.java b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerInMemory.java new file mode 100644 index 00000000000..2f49a52ef93 --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerInMemory.java @@ -0,0 +1,130 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.operator.events.EventReason.AutoscalingNotAllowed; +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.MultipleReplicasNotAllowed; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; +import org.projectnessie.operator.reconciler.AbstractReconcilerUnitTests; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; +import org.projectnessie.operator.reconciler.nessie.resource.NessieBuilder; + +@QuarkusTest +@TestProfile(AbstractReconcilerUnitTests.Profile.class) +@WithKubernetesTestServer +class TestNessieReconcilerInMemory extends AbstractReconcilerUnitTests { + + private static final String PREFIX = "/org/projectnessie/operator/tests/fixtures/inmemory/"; + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml"); + } + + @Override + protected void assertResourcesCreated() { + checkInMemoryWarning(); + checkAutoscalingWarning(); + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test")); + emulateSideCarInjection(); + checkDeployment( + load(client.apps().deployments(), PREFIX + "deployment.yaml"), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkEvents(CreatingConfigMap, CreatingDeployment, CreatingService, ReconcileSuccess); + checkNotCreated(client.serviceAccounts()); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.network().v1().ingresses()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + checkNotCreated(client.monitoring().serviceMonitors()); + } + + private void checkInMemoryWarning() { + if (primary.getSpec().size() == 2) { + checkEvent( + MultipleReplicasNotAllowed, + "InMemory version store can only be used with a single replica."); + refreshPrimary(); + client + .resource(primary) + .edit(p -> new NessieBuilder(p).editOrNewSpec().withSize(1).endSpec().build()); + } + } + + private void checkAutoscalingWarning() { + if (primary.getSpec().autoscaling().enabled()) { + checkEvent(AutoscalingNotAllowed, "Autoscaling is not allowed with InMemory version store."); + refreshPrimary(); + primary = + client + .resource(primary) + .edit( + p -> + new NessieBuilder(p) + .editOrNewSpec() + .editOrNewAutoscaling() + .withEnabled(false) + .endAutoscaling() + .endSpec() + .build()); + } + } + + private void emulateSideCarInjection() { + Deployment actual = get(client.apps().deployments(), "nessie-test"); + assertThat(actual).isNotNull(); + if (actual.getSpec().getTemplate().getSpec().getInitContainers().isEmpty()) { + client + .resource(actual) + .edit( + d -> + d.edit() + .editSpec() + .editTemplate() + .editSpec() + .withInitContainers( + new ContainerBuilder() + .withName("sidecar") + .withImage("busybox") + .withImagePullPolicy("IfNotPresent") + .withCommand("sleep", "3600") + .build()) + .endSpec() + .endTemplate() + .endSpec() + .build()); + } + } +} diff --git a/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerJdbc.java b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerJdbc.java new file mode 100644 index 00000000000..09875f39ed6 --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerJdbc.java @@ -0,0 +1,96 @@ +/* + * 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; + +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingHPA; +import static org.projectnessie.operator.events.EventReason.CreatingIngress; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceAccount; +import static org.projectnessie.operator.events.EventReason.CreatingServiceMonitor; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; +import org.projectnessie.operator.reconciler.AbstractReconcilerUnitTests; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; + +@QuarkusTest +@TestProfile(AbstractReconcilerUnitTests.Profile.class) +@WithKubernetesTestServer(setup = TestNessieReconcilerJdbc.Setup.class) +class TestNessieReconcilerJdbc extends AbstractReconcilerUnitTests { + + private static final String PREFIX = "/org/projectnessie/operator/tests/fixtures/jdbc/"; + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml"); + } + + @Override + protected void assertResourcesCreated() { + checkServiceAccount( + load(client.serviceAccounts(), PREFIX + "service-account.yaml"), + get(client.serviceAccounts(), "nessie-test-custom-service-account")); + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test")); + checkDeployment( + load(client.apps().deployments(), PREFIX + "deployment.yaml"), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkIngress( + load(client.network().v1().ingresses(), PREFIX + "ingress.yaml"), + get(client.network().v1().ingresses(), "nessie-test")); + checkServiceMonitor( + load(client.monitoring().serviceMonitors(), PREFIX + "service-monitor.yaml"), + get(client.monitoring().serviceMonitors(), "nessie-test")); + checkAutoscaler( + load(client.autoscaling().v2().horizontalPodAutoscalers(), PREFIX + "autoscaler.yaml"), + get(client.autoscaling().v2().horizontalPodAutoscalers(), "nessie-test")); + checkEvents( + CreatingServiceAccount, + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + CreatingIngress, + CreatingServiceMonitor, + CreatingHPA, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + } + + public static class Setup extends AbstractReconcilerUnitTests.Setup { + @Override + public void accept(KubernetesServer server) { + reportApiSupported(server, "networking.k8s.io", "v1"); + reportApiSupported(server, "autoscaling", "v2"); + reportApiSupported(server, "monitoring.coreos.com", "v1"); + } + } +} diff --git a/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerMongo.java b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerMongo.java new file mode 100644 index 00000000000..021828ba2d8 --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerMongo.java @@ -0,0 +1,96 @@ +/* + * 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; + +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingHPA; +import static org.projectnessie.operator.events.EventReason.CreatingIngress; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceAccount; +import static org.projectnessie.operator.events.EventReason.CreatingServiceMonitor; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; +import org.projectnessie.operator.reconciler.AbstractReconcilerUnitTests; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; + +@QuarkusTest +@TestProfile(AbstractReconcilerUnitTests.Profile.class) +@WithKubernetesTestServer(setup = TestNessieReconcilerMongo.Setup.class) +class TestNessieReconcilerMongo extends AbstractReconcilerUnitTests { + + private static final String PREFIX = "/org/projectnessie/operator/tests/fixtures/mongo/"; + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml"); + } + + @Override + protected void assertResourcesCreated() { + checkServiceAccount( + load(client.serviceAccounts(), PREFIX + "service-account.yaml"), + get(client.serviceAccounts(), "nessie-test")); + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test")); + checkDeployment( + load(client.apps().deployments(), PREFIX + "deployment.yaml"), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkIngress( + load(client.network().v1().ingresses(), PREFIX + "ingress.yaml"), + get(client.network().v1().ingresses(), "nessie-test")); + checkServiceMonitor( + load(client.monitoring().serviceMonitors(), PREFIX + "service-monitor.yaml"), + get(client.monitoring().serviceMonitors(), "nessie-test")); + checkAutoscaler( + load(client.autoscaling().v2beta2().horizontalPodAutoscalers(), PREFIX + "autoscaler.yaml"), + get(client.autoscaling().v2beta2().horizontalPodAutoscalers(), "nessie-test")); + checkEvents( + CreatingServiceAccount, + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + CreatingIngress, + CreatingServiceMonitor, + CreatingHPA, + ReconcileSuccess); + checkNotCreated(client.persistentVolumeClaims()); + checkNotCreated(client.network().v1beta1().ingresses()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + } + + public static class Setup extends AbstractReconcilerUnitTests.Setup { + @Override + public void accept(KubernetesServer server) { + reportApiSupported(server, "networking.k8s.io", "v1"); + reportApiSupported(server, "autoscaling", "v2beta2"); + reportApiSupported(server, "monitoring.coreos.com", "v1"); + } + } +} diff --git a/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerRocks.java b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerRocks.java new file mode 100644 index 00000000000..7dfb75ff958 --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/reconciler/nessie/TestNessieReconcilerRocks.java @@ -0,0 +1,97 @@ +/* + * 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; + +import static org.projectnessie.operator.events.EventReason.CreatingConfigMap; +import static org.projectnessie.operator.events.EventReason.CreatingDeployment; +import static org.projectnessie.operator.events.EventReason.CreatingIngress; +import static org.projectnessie.operator.events.EventReason.CreatingMgmtService; +import static org.projectnessie.operator.events.EventReason.CreatingPersistentVolumeClaim; +import static org.projectnessie.operator.events.EventReason.CreatingService; +import static org.projectnessie.operator.events.EventReason.CreatingServiceAccount; +import static org.projectnessie.operator.events.EventReason.CreatingServiceMonitor; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.kubernetes.client.WithKubernetesTestServer; +import org.projectnessie.operator.reconciler.AbstractReconcilerUnitTests; +import org.projectnessie.operator.reconciler.nessie.resource.Nessie; + +@QuarkusTest +@TestProfile(AbstractReconcilerUnitTests.Profile.class) +@WithKubernetesTestServer(setup = TestNessieReconcilerRocks.Setup.class) +class TestNessieReconcilerRocks extends AbstractReconcilerUnitTests { + + private static final String PREFIX = "/org/projectnessie/operator/tests/fixtures/rocks/"; + + @Override + protected Nessie newPrimary() { + return load(client.resources(Nessie.class), PREFIX + "nessie.yaml"); + } + + @Override + protected void assertResourcesCreated() { + checkServiceAccount( + load(client.serviceAccounts(), PREFIX + "service-account.yaml"), + get(client.serviceAccounts(), "nessie-test-custom-service-account")); + checkConfigMap( + load(client.configMaps(), PREFIX + "config-map.yaml"), + get(client.configMaps(), "nessie-test")); + checkPvc( + load(client.persistentVolumeClaims(), PREFIX + "pvc.yaml"), + get(client.persistentVolumeClaims(), "nessie-test")); + checkDeployment( + load(client.apps().deployments(), PREFIX + "deployment.yaml"), + get(client.apps().deployments(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service.yaml"), get(client.services(), "nessie-test")); + checkService( + load(client.services(), PREFIX + "service-mgmt.yaml"), + get(client.services(), "nessie-test-mgmt")); + checkIngress( + load(client.network().v1beta1().ingresses(), PREFIX + "ingress.yaml"), + get(client.network().v1beta1().ingresses(), "nessie-test")); + checkServiceMonitor( + load( + client.monitoring().serviceMonitors(), + "/org/projectnessie/operator/tests/fixtures/jdbc/service-monitor.yaml"), + get(client.monitoring().serviceMonitors(), "nessie-test")); + checkEvents( + CreatingServiceAccount, + CreatingPersistentVolumeClaim, + CreatingConfigMap, + CreatingDeployment, + CreatingService, + CreatingMgmtService, + CreatingIngress, + CreatingServiceMonitor, + ReconcileSuccess); + checkNotCreated(client.network().v1().ingresses()); + checkNotCreated(client.autoscaling().v2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta2().horizontalPodAutoscalers()); + checkNotCreated(client.autoscaling().v2beta1().horizontalPodAutoscalers()); + } + + public static class Setup extends AbstractReconcilerUnitTests.Setup { + @Override + public void accept(KubernetesServer server) { + reportApiSupported(server, "networking.k8s.io", "v1beta1"); + reportApiSupported(server, "monitoring.coreos.com", "v1"); + } + } +} diff --git a/operator/src/test/java/org/projectnessie/operator/utils/TestEventUtils.java b/operator/src/test/java/org/projectnessie/operator/utils/TestEventUtils.java new file mode 100644 index 00000000000..d3c2a8d7076 --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/utils/TestEventUtils.java @@ -0,0 +1,87 @@ +/* + * 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.utils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.projectnessie.operator.events.EventReason.AutoscalingNotAllowed; +import static org.projectnessie.operator.events.EventReason.ReconcileError; +import static org.projectnessie.operator.events.EventReason.ReconcileSuccess; + +import java.time.ZonedDateTime; +import org.junit.jupiter.api.Test; +import org.projectnessie.operator.exception.InvalidSpecException; +import org.projectnessie.operator.reconciler.nessie.resource.NessieBuilder; + +class TestEventUtils { + + @Test + void formatTime() { + assertThat(EventUtils.formatTime(ZonedDateTime.parse("2006-01-02T15:04:05Z"))) + .isEqualTo("2006-01-02T15:04:05Z"); + assertThat(EventUtils.formatTime(ZonedDateTime.parse("2006-01-02T15:04:05+07:00"))) + .isEqualTo("2006-01-02T15:04:05+07:00"); + assertThat(EventUtils.formatTime(ZonedDateTime.parse("2006-01-02T15:04:05.999999Z"))) + .isEqualTo("2006-01-02T15:04:05Z"); + assertThat(EventUtils.formatTime(ZonedDateTime.parse("2006-01-02T15:04:05.999999+07:00"))) + .isEqualTo("2006-01-02T15:04:05+07:00"); + } + + @Test + void formatMicroTime() { + assertThat(EventUtils.formatMicroTime(ZonedDateTime.parse("2006-01-02T15:04:05Z"))) + .isEqualTo("2006-01-02T15:04:05.000000Z"); + assertThat(EventUtils.formatMicroTime(ZonedDateTime.parse("2006-01-02T15:04:05+07:00"))) + .isEqualTo("2006-01-02T15:04:05.000000+07:00"); + assertThat(EventUtils.formatMicroTime(ZonedDateTime.parse("2006-01-02T15:04:05.999999Z"))) + .isEqualTo("2006-01-02T15:04:05.999999Z"); + assertThat(EventUtils.formatMicroTime(ZonedDateTime.parse("2006-01-02T15:04:05.999999+07:00"))) + .isEqualTo("2006-01-02T15:04:05.999999+07:00"); + } + + @Test + void eventName() { + assertThat( + EventUtils.eventName( + new NessieBuilder().withNewMetadata().withUid("1234").endMetadata().build(), + ReconcileSuccess)) + .isEqualTo("nessie-1234-ReconcileSuccess"); + } + + @Test + void reasonFromEventName() { + assertThat(EventUtils.reasonFromEventName("nessie-1234-ReconcileSuccess")) + .isEqualTo(ReconcileSuccess); + } + + @Test + void errorReason() { + assertThat( + EventUtils.errorReason(new InvalidSpecException(AutoscalingNotAllowed, "irrelevant"))) + .isEqualTo(AutoscalingNotAllowed); + assertThat(EventUtils.errorReason(new RuntimeException("test"))).isEqualTo(ReconcileError); + } + + @Test + void formatMessage() { + assertThat(EventUtils.formatMessage("test")).isEqualTo("test"); + assertThat(EventUtils.formatMessage("test %s %d", "123", 456)).isEqualTo("test 123 456"); + assertThat(EventUtils.formatMessage("test %s %d", null, null)).isEqualTo("test null null"); + assertThat(EventUtils.formatMessage("x".repeat(1024))).isEqualTo("x".repeat(1024)); + assertThat(EventUtils.formatMessage("x".repeat(1025))) + .isEqualTo("x".repeat(1009) + "... [truncated]") + .hasSize(1024); + } +} diff --git a/operator/src/test/java/org/projectnessie/operator/utils/TestResourceUtils.java b/operator/src/test/java/org/projectnessie/operator/utils/TestResourceUtils.java new file mode 100644 index 00000000000..1a82e595a17 --- /dev/null +++ b/operator/src/test/java/org/projectnessie/operator/utils/TestResourceUtils.java @@ -0,0 +1,62 @@ +/* + * 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.utils; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.projectnessie.operator.exception.InvalidSpecException; + +class TestResourceUtils { + + @Test + void validateName() { + assertThatCode(() -> ResourceUtils.validateName("a")).doesNotThrowAnyException(); + assertThatCode(() -> ResourceUtils.validateName("a1")).doesNotThrowAnyException(); + assertThatCode(() -> ResourceUtils.validateName("a1-b")).doesNotThrowAnyException(); + assertThatCode(() -> ResourceUtils.validateName("a1-b2")).doesNotThrowAnyException(); + // wrong chars + assertThatThrownBy(() -> ResourceUtils.validateName("-")) + .isInstanceOf(InvalidSpecException.class) + .hasMessage( + "Resource name must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character"); + assertThatThrownBy(() -> ResourceUtils.validateName("a-")) + .isInstanceOf(InvalidSpecException.class) + .hasMessage( + "Resource name must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character"); + assertThatThrownBy(() -> ResourceUtils.validateName("-a")) + .isInstanceOf(InvalidSpecException.class) + .hasMessage( + "Resource name must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character"); + assertThatThrownBy(() -> ResourceUtils.validateName("1a")) + .isInstanceOf(InvalidSpecException.class) + .hasMessage( + "Resource name must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character"); + assertThatThrownBy(() -> ResourceUtils.validateName("a_b")) + .isInstanceOf(InvalidSpecException.class) + .hasMessage( + "Resource name must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character"); + // lengths + assertThatCode(() -> ResourceUtils.validateName("a".repeat(63))).doesNotThrowAnyException(); + assertThatThrownBy(() -> ResourceUtils.validateName("a".repeat(64))) + .isInstanceOf(InvalidSpecException.class) + .hasMessage("Resource name cannot be longer than 63 characters"); + assertThatThrownBy(() -> ResourceUtils.validateName("a".repeat(11), 10)) + .isInstanceOf(InvalidSpecException.class) + .hasMessage("Resource name cannot be longer than 10 characters"); + } +} diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/autoscaler.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/autoscaler.yaml new file mode 100644 index 00000000000..f1d9b3d2308 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/autoscaler.yaml @@ -0,0 +1,37 @@ +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: nessie-test +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nessie-test + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + targetAverageUtilization: 80 + - type: Resource + resource: + name: memory + targetAverageUtilization: 80 +status: + conditions: + - lastTransitionTime: "2024-02-19T16:56:33Z" + message: the HPA controller was able to get the target's current scale + reason: SucceededGetScale + status: "True" + type: AbleToScale + - lastTransitionTime: "2024-02-19T16:56:33Z" + message: 'the HPA was unable to compute the replica count: failed to get cpu utilization: + unable to get metrics for resource cpu: unable to fetch metrics from resource + metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)' + reason: FailedGetResourceMetric + status: "False" + type: ScalingActive + currentMetrics: null + currentReplicas: 1 + desiredReplicas: 0 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/config-map.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/config-map.yaml new file mode 100644 index 00000000000..e82434a4d5c --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/config-map.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.server.authentication.enabled=true + nessie.server.authorization.enabled=true + nessie.server.authorization.rules.allowViewingBranch=op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + nessie.server.default-branch=my-branch + nessie.version.store.persist.bigtable.app-profile-id=my-app-profile + nessie.version.store.persist.bigtable.instance-id=my-instance + nessie.version.store.persist.cache-capacity-fraction-adjust-mb=256 + nessie.version.store.persist.cache-capacity-fraction-min-size-mb=64 + nessie.version.store.persist.cache-capacity-fraction-of-heap=0.7 + nessie.version.store.persist.cache-capacity-mb=1024 + nessie.version.store.persist.repository-id=my-repository + nessie.version.store.type=BIGTABLE + quarkus.google.cloud.project-id=my-project + quarkus.log.category."org.projectnessie".level=TRACE + quarkus.log.console.format=%d{HH:mm:ss} %s%e%n + quarkus.log.console.level=DEBUG + quarkus.log.file.level=DEBUG + quarkus.log.level=DEBUG + quarkus.oidc.auth-server-url=http://keycloak:8080/auth/realms/nessie + quarkus.oidc.client-id=quarkus-app + quarkus.otel.exporter.otlp.traces.endpoint=https://otlp-collector:4317 + quarkus.otel.resource.attributes=foo=bar,service.name=nessie-test + quarkus.otel.traces.sampler=parentbased_always_on diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/deployment.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/deployment.yaml new file mode 100644 index 00000000000..fe0d2080c45 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/deployment.yaml @@ -0,0 +1,126 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + # replicas not set because of HPA + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + foo: bar + annotations: + foo: bar + projectnessie.org/config-checksum: sha256:9e456da8e922c81f25312279489d3a4c80ed880dc40ceb5a4dbf19627235a1bb + spec: + securityContext: + fsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" + containers: + - name: nessie + image: projectnessie/nessie:1.2.3 + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: GOOGLE_APPLICATION_CREDENTIALS + value: "/bigtable-nessie/sa_credentials.json" + - name: QUARKUS_PROFILE + value: "prod" + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + - name: bigtable-creds + mountPath: /bigtable-nessie + livenessProbe: + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 1 + periodSeconds: 2 + timeoutSeconds: 3 + successThreshold: 4 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + volumes: + - name: nessie-config + configMap: + name: nessie-test + - name: bigtable-creds + secret: + secretName: nessie-db-credentials + items: + - key: key.json + path: sa_credentials.json + serviceAccountName: default +status: + observedGeneration: 1 + replicas: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + conditions: + - type: Available + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:21Z' + reason: MinimumReplicasAvailable + message: Deployment has minimum availability. + - type: Progressing + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:18Z' + reason: NewReplicaSetAvailable + message: ReplicaSet "nessie-test-abcdefg" has successfully progressed. diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/ingress.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/ingress.yaml new file mode 100644 index 00000000000..f6db8e74a81 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/ingress.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + kubernetes.io/ingress.class: nginx +spec: + rules: + - host: nessie.example.com + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: nessie-test + port: + number: 19120 + tls: + - hosts: + - nessie.example.com + secretName: nessie-test-tls +status: + loadBalancer: + ingress: + - ip: 192.168.49.2 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/nessie.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/nessie.yaml new file mode 100644 index 00000000000..c21408c0429 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/nessie.yaml @@ -0,0 +1,119 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + size: 1 + logLevel: DEBUG + service: + type: ClusterIP + sessionAffinity: ClientIP + port: 19120 + labels: + foo: bar + annotations: + foo: bar + versionStore: + type: BigTable + bigTable: + projectId: my-project + instanceId: my-instance + appProfileId: my-app-profile + credentials: + secret: nessie-db-credentials + key: key.json + ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: nginx + rules: + - host: nessie.example.com + paths: + - / + tls: + - secret: nessie-test-tls + hosts: + - nessie.example.com + authentication: + enabled: true + oidcAuthServerUrl: http://keycloak:8080/auth/realms/nessie + oidcClientId: quarkus-app + authorization: + enabled: true + rules: + allowViewingBranch: op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + telemetry: + enabled: true + endpoint: https://otlp-collector:4317 + sample: "all" + attributes: + foo: "bar" + monitoring: + enabled: true + labels: + foo: bar + interval: 1s + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 2 + targetCpuUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + extraEnv: + - name: QUARKUS_PROFILE + value: "prod" + advancedConfig: + nessie.server.default-branch: my-branch + nessie.version.store.persist.repository-id: my-repository + nessie.version.store.persist.cache-capacity-mb: 1024 + nessie.version.store.persist.cache-capacity-fraction-of-heap: 0.7 + nessie.version.store.persist.cache-capacity-fraction-adjust-mb: 256 + nessie.version.store.persist.cache-capacity-fraction-min-size-mb: 64 + quarkus: + log: + console.format: "%d{HH:mm:ss} %s%e%n" + category."org.projectnessie".level: "TRACE" + deployment: + image: + repository: projectnessie/nessie + tag: 1.2.3 + pullPolicy: Never + labels: + foo: bar + annotations: + foo: bar + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + livenessProbe: + initialDelaySeconds: 1 + periodSeconds: 2 + timeoutSeconds: 3 + successThreshold: 4 + failureThreshold: 5 + readinessProbe: {} + podSecurityContext: + fsGroup: 1000 + containerSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service-mgmt.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service-mgmt.yaml new file mode 100644 index 00000000000..62c96fd5125 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service-mgmt.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar + annotations: + foo: bar +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service-monitor.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service-monitor.yaml new file mode 100644 index 00000000000..8852220e45b --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service-monitor.yaml @@ -0,0 +1,26 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + namespaceSelector: + matchNames: + - @namespace@ + endpoints: + - port: nessie-mgmt + scheme: http + path: /q/metrics + interval: 1s diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service.yaml new file mode 100644 index 00000000000..f900a61c77a --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/bigtable/service.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar + annotations: + foo: bar +spec: + ports: + - name: nessie-server + protocol: TCP + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + type: ClusterIP + sessionAffinity: ClientIP diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/autoscaler.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/autoscaler.yaml new file mode 100644 index 00000000000..f1d9b3d2308 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/autoscaler.yaml @@ -0,0 +1,37 @@ +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: nessie-test +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nessie-test + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + targetAverageUtilization: 80 + - type: Resource + resource: + name: memory + targetAverageUtilization: 80 +status: + conditions: + - lastTransitionTime: "2024-02-19T16:56:33Z" + message: the HPA controller was able to get the target's current scale + reason: SucceededGetScale + status: "True" + type: AbleToScale + - lastTransitionTime: "2024-02-19T16:56:33Z" + message: 'the HPA was unable to compute the replica count: failed to get cpu utilization: + unable to get metrics for resource cpu: unable to fetch metrics from resource + metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)' + reason: FailedGetResourceMetric + status: "False" + type: ScalingActive + currentMetrics: null + currentReplicas: 1 + desiredReplicas: 0 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/config-map.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/config-map.yaml new file mode 100644 index 00000000000..5d7302f37c6 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/config-map.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.server.authentication.enabled=true + nessie.server.authorization.enabled=true + nessie.server.authorization.rules.allowViewingBranch=op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + nessie.server.default-branch=my-branch + nessie.version.store.persist.cache-capacity-fraction-adjust-mb=128 + nessie.version.store.persist.cache-capacity-fraction-min-size-mb=128 + nessie.version.store.persist.cache-capacity-fraction-of-heap=0.8 + nessie.version.store.persist.repository-id=my-repository + nessie.version.store.type=CASSANDRA + quarkus.cassandra.auth.username=cassandra + quarkus.cassandra.contact-points=cassandra-0.cassandra.default.svc.cluster.local,cassandra-1.cassandra.default.svc.cluster.local,cassandra-2.cassandra.default.svc.cluster.local + quarkus.cassandra.keyspace=ks1 + quarkus.cassandra.local-datacenter=datacenter1 + quarkus.log.category."org.projectnessie".level=TRACE + quarkus.log.console.format=%d{HH:mm:ss} %s%e%n + quarkus.log.console.level=DEBUG + quarkus.log.file.level=DEBUG + quarkus.log.level=DEBUG + quarkus.oidc.auth-server-url=http://keycloak:8080/auth/realms/nessie + quarkus.oidc.client-id=quarkus-app + quarkus.otel.exporter.otlp.traces.endpoint=https://otlp-collector:4317 + quarkus.otel.resource.attributes=foo=bar,service.name=nessie-test + quarkus.otel.traces.sampler=parentbased_always_off diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/deployment.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/deployment.yaml new file mode 100644 index 00000000000..7c2f1466dd4 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/deployment.yaml @@ -0,0 +1,126 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + # replicas not set because of HPA + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + foo: bar + annotations: + foo: bar + projectnessie.org/config-checksum: sha256:6f064ee4496ef5344dc2717b535b5e69cb7d558fa813be4bf6f5e66b750296c5 + spec: + securityContext: + fsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" + containers: + - name: nessie + image: projectnessie/nessie:1.2.3 + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: quarkus.cassandra.auth.password + valueFrom: + secretKeyRef: + name: nessie-db-credentials + key: password + - name: QUARKUS_PROFILE + value: "prod" + livenessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 2 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 10 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 3 + periodSeconds: 45 + successThreshold: 1 + timeoutSeconds: 10 + serviceAccountName: nessie-test-custom-service-account + volumes: + - name: nessie-config + configMap: + name: nessie-test +status: + observedGeneration: 1 + replicas: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + conditions: + - type: Available + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:21Z' + reason: MinimumReplicasAvailable + message: Deployment has minimum availability. + - type: Progressing + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:18Z' + reason: NewReplicaSetAvailable + message: ReplicaSet "nessie-test-abcdefg" has successfully progressed. diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/ingress.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/ingress.yaml new file mode 100644 index 00000000000..f6db8e74a81 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/ingress.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + kubernetes.io/ingress.class: nginx +spec: + rules: + - host: nessie.example.com + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: nessie-test + port: + number: 19120 + tls: + - hosts: + - nessie.example.com + secretName: nessie-test-tls +status: + loadBalancer: + ingress: + - ip: 192.168.49.2 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/nessie.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/nessie.yaml new file mode 100644 index 00000000000..001e68bea7b --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/nessie.yaml @@ -0,0 +1,118 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + size: 1 + logLevel: DEBUG + service: + type: ClusterIP + sessionAffinity: ClientIP + port: 19120 + labels: {} + annotations: {} + versionStore: + type: Cassandra + cache: + enabled: true + heapFraction: 800m + minSize: 128Mi + minFreeHeap: 128Mi + cassandra: + localDatacenter: datacenter1 + keyspace: ks1 + contactPoints: + - cassandra-0.cassandra.default.svc.cluster.local + - cassandra-1.cassandra.default.svc.cluster.local + - cassandra-2.cassandra.default.svc.cluster.local + username: cassandra + password: + secret: nessie-db-credentials + key: password + ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: nginx + rules: + - host: nessie.example.com + paths: + - / + tls: + - secret: nessie-test-tls + hosts: + - nessie.example.com + authentication: + enabled: true + oidcAuthServerUrl: http://keycloak:8080/auth/realms/nessie + oidcClientId: quarkus-app + authorization: + enabled: true + rules: + allowViewingBranch: op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + telemetry: + enabled: true + endpoint: https://otlp-collector:4317 + sample: "none" + attributes: + foo: "bar" + monitoring: + enabled: true + labels: + foo: bar + interval: 1s + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 2 + targetCpuUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + extraEnv: + - name: QUARKUS_PROFILE + value: "prod" + advancedConfig: + nessie.server.default-branch: my-branch + nessie.version.store.persist.repository-id: my-repository + quarkus: + log: + console.format: "%d{HH:mm:ss} %s%e%n" + category."org.projectnessie".level: "TRACE" + deployment: + image: + repository: projectnessie/nessie + tag: 1.2.3 + pullPolicy: Never + labels: + foo: bar + annotations: + foo: bar + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + serviceAccount: + create: false + name: nessie-test-custom-service-account + podSecurityContext: + fsGroup: 1000 + containerSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service-mgmt.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service-monitor.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service-monitor.yaml new file mode 100644 index 00000000000..8852220e45b --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service-monitor.yaml @@ -0,0 +1,26 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + namespaceSelector: + matchNames: + - @namespace@ + endpoints: + - port: nessie-mgmt + scheme: http + path: /q/metrics + interval: 1s diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service.yaml new file mode 100644 index 00000000000..50e230895cf --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/cassandra/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-server + protocol: TCP + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + type: ClusterIP + sessionAffinity: ClientIP diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/config-map.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/config-map.yaml new file mode 100644 index 00000000000..be058ae3b0b --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/config-map.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + quarkus.oidc.tenant-enabled=false + quarkus.otel.sdk.disabled=true diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/deployment.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/deployment.yaml new file mode 100644 index 00000000000..994c759384b --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/deployment.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + annotations: + projectnessie.org/config-checksum: sha256:1c4bfa24325a950dab892feb4d17bdedf928462a43d87f16d0585b3aace1b9c9 + spec: + containers: + - name: nessie + image: projectnessie/nessie:latest + imagePullPolicy: Always + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + - name: nessie-debug + containerPort: 5009 + protocol: TCP + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + env: + - name: JAVA_DEBUG + value: "true" + - name: JAVA_DEBUG_PORT + value: "*:5009" + - name: JAVA_OPTS_APPEND + value: "-XX:MaxRAMPercentage=75.0" + livenessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 2 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 10 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 3 + periodSeconds: 45 + successThreshold: 1 + timeoutSeconds: 10 + # "injected" side-car + initContainers: + - name: sidecar + image: busybox + imagePullPolicy: IfNotPresent + command: [ 'sleep', '3600' ] + serviceAccountName: default + volumes: + - name: nessie-config + configMap: + name: nessie-test +status: + observedGeneration: 1 + replicas: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + conditions: + - type: Available + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:21Z' + reason: MinimumReplicasAvailable + message: Deployment has minimum availability. + - type: Progressing + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:18Z' + reason: NewReplicaSetAvailable + message: ReplicaSet "nessie-test-abcdefg" has successfully progressed. diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/nessie.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/nessie.yaml new file mode 100644 index 00000000000..4208784ed64 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/nessie.yaml @@ -0,0 +1,20 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + size: 2 + service: + sessionAffinity: ClientIP + autoscaling: + enabled: true + targetMemoryUtilizationPercentage: 80 + deployment: + image: + repository: projectnessie/nessie + tag: latest + remoteDebug: + enabled: true + port: 5009 + jvmOptions: + - -XX:MaxRAMPercentage=75.0 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/service-mgmt.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/service.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/service.yaml new file mode 100644 index 00000000000..50e230895cf --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/inmemory/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-server + protocol: TCP + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + type: ClusterIP + sessionAffinity: ClientIP diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/autoscaler.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/autoscaler.yaml new file mode 100644 index 00000000000..6da5622f333 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/autoscaler.yaml @@ -0,0 +1,41 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: nessie-test +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nessie-test + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 80 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 +status: + conditions: + - lastTransitionTime: "2024-02-19T16:56:33Z" + message: the HPA controller was able to get the target's current scale + reason: SucceededGetScale + status: "True" + type: AbleToScale + - lastTransitionTime: "2024-02-19T16:56:33Z" + message: 'the HPA was unable to compute the replica count: failed to get cpu utilization: + unable to get metrics for resource cpu: unable to fetch metrics from resource + metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)' + reason: FailedGetResourceMetric + status: "False" + type: ScalingActive + currentMetrics: null + currentReplicas: 1 + desiredReplicas: 0 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/config-map.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/config-map.yaml new file mode 100644 index 00000000000..f30af2fb37e --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/config-map.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.server.authentication.enabled=true + nessie.server.authorization.enabled=true + nessie.server.authorization.rules.allowViewingBranch=op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + nessie.server.default-branch=my-branch + nessie.version.store.persist.cache-capacity-mb=512 + nessie.version.store.persist.jdbc.datasource=postgresql + nessie.version.store.persist.repository-id=my-repository + nessie.version.store.type=JDBC + quarkus.datasource.postgresql.jdbc.url=jdbc:postgresql://nessie-postgresql.default.svc.cluster.local:5432/nessie + quarkus.datasource.postgresql.username=postgres + quarkus.log.category."org.projectnessie".level=TRACE + quarkus.log.console.format=%d{HH:mm:ss} %s%e%n + quarkus.log.console.level=DEBUG + quarkus.log.file.level=DEBUG + quarkus.log.level=DEBUG + quarkus.oidc.auth-server-url=http://keycloak:8080/auth/realms/nessie + quarkus.oidc.client-id=quarkus-app + quarkus.otel.exporter.otlp.traces.endpoint=https://otlp-collector:4317 + quarkus.otel.resource.attributes=foo=bar,service.name=nessie-test + quarkus.otel.traces.sampler=parentbased_traceidratio + quarkus.otel.traces.sampler.arg=0.5d diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/deployment.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/deployment.yaml new file mode 100644 index 00000000000..04e42f2f589 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/deployment.yaml @@ -0,0 +1,126 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + # replicas not set because of HPA + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + foo: bar + annotations: + foo: bar + projectnessie.org/config-checksum: sha256:119d91df7eab1a95f3052a454a1348179f6a4fbfe45ddb23c6607afd0c6f77c5 + spec: + securityContext: + fsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" + containers: + - name: nessie + image: projectnessie/nessie:1.2.3 + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: QUARKUS_PROFILE + value: "prod" + - name: quarkus.datasource.postgresql.password + valueFrom: + secretKeyRef: + name: nessie-db-credentials + key: password + livenessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 2 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 10 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 3 + periodSeconds: 45 + successThreshold: 1 + timeoutSeconds: 10 + serviceAccountName: nessie-test-custom-service-account + volumes: + - name: nessie-config + configMap: + name: nessie-test +status: + observedGeneration: 1 + replicas: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + conditions: + - type: Available + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:21Z' + reason: MinimumReplicasAvailable + message: Deployment has minimum availability. + - type: Progressing + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:18Z' + reason: NewReplicaSetAvailable + message: ReplicaSet "nessie-test-abcdefg" has successfully progressed. diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/ingress.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/ingress.yaml new file mode 100644 index 00000000000..f6db8e74a81 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/ingress.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + kubernetes.io/ingress.class: nginx +spec: + rules: + - host: nessie.example.com + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: nessie-test + port: + number: 19120 + tls: + - hosts: + - nessie.example.com + secretName: nessie-test-tls +status: + loadBalancer: + ingress: + - ip: 192.168.49.2 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/nessie.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/nessie.yaml new file mode 100644 index 00000000000..521cbde0786 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/nessie.yaml @@ -0,0 +1,112 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + size: 1 + logLevel: DEBUG + service: + type: ClusterIP + sessionAffinity: ClientIP + port: 19120 + labels: {} + annotations: {} + versionStore: + type: Jdbc + cache: + fixedSize: 512Mi + jdbc: + url: jdbc:postgresql://nessie-postgresql.default.svc.cluster.local:5432/nessie + username: postgres + password: + secret: nessie-db-credentials + key: password + ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: nginx + rules: + - host: nessie.example.com + paths: + - / + tls: + - secret: nessie-test-tls + hosts: + - nessie.example.com + authentication: + enabled: true + oidcAuthServerUrl: http://keycloak:8080/auth/realms/nessie + oidcClientId: quarkus-app + authorization: + enabled: true + rules: + allowViewingBranch: op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + telemetry: + enabled: true + endpoint: https://otlp-collector:4317 + sample: "0.5d" + attributes: + foo: "bar" + monitoring: + enabled: true + labels: + foo: bar + interval: 1s + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 2 + targetCpuUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + extraEnv: + - name: QUARKUS_PROFILE + value: "prod" + advancedConfig: + nessie.server.default-branch: my-branch + nessie.version.store.persist.repository-id: my-repository + quarkus: + log: + console.format: "%d{HH:mm:ss} %s%e%n" + category."org.projectnessie".level: "TRACE" + deployment: + image: + repository: projectnessie/nessie + tag: 1.2.3 + pullPolicy: Never + labels: + foo: bar + annotations: + foo: bar + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + serviceAccount: + create: true + name: nessie-test-custom-service-account + annotations: + foo: bar + podSecurityContext: + fsGroup: 1000 + containerSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-account.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-account.yaml new file mode 100644 index 00000000000..0818154f142 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-account.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nessie-test-custom-service-account + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + foo: bar diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-mgmt.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-monitor.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-monitor.yaml new file mode 100644 index 00000000000..8852220e45b --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service-monitor.yaml @@ -0,0 +1,26 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + namespaceSelector: + matchNames: + - @namespace@ + endpoints: + - port: nessie-mgmt + scheme: http + path: /q/metrics + interval: 1s diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service.yaml new file mode 100644 index 00000000000..50e230895cf --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/jdbc/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-server + protocol: TCP + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + type: ClusterIP + sessionAffinity: ClientIP diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/autoscaler.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/autoscaler.yaml new file mode 100644 index 00000000000..0a228cd91ba --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/autoscaler.yaml @@ -0,0 +1,41 @@ +apiVersion: autoscaling/v2beta2 +kind: HorizontalPodAutoscaler +metadata: + name: nessie-test +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: nessie-test + minReplicas: 1 + maxReplicas: 2 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 80 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 +status: + conditions: + - lastTransitionTime: "2024-02-19T16:56:33Z" + message: the HPA controller was able to get the target's current scale + reason: SucceededGetScale + status: "True" + type: AbleToScale + - lastTransitionTime: "2024-02-19T16:56:33Z" + message: 'the HPA was unable to compute the replica count: failed to get cpu utilization: + unable to get metrics for resource cpu: unable to fetch metrics from resource + metrics API: the server could not find the requested resource (get pods.metrics.k8s.io)' + reason: FailedGetResourceMetric + status: "False" + type: ScalingActive + currentMetrics: null + currentReplicas: 1 + desiredReplicas: 0 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/config-map.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/config-map.yaml new file mode 100644 index 00000000000..6c22154b02f --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/config-map.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.server.authentication.enabled=true + nessie.server.authorization.enabled=true + nessie.server.authorization.rules.allowViewingBranch=op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + nessie.server.default-branch=my-branch + nessie.version.store.persist.cache-capacity-mb=0 + nessie.version.store.persist.repository-id=my-repository + nessie.version.store.type=MONGODB + quarkus.log.category."org.projectnessie".level=TRACE + quarkus.log.console.format=%d{HH:mm:ss} %s%e%n + quarkus.log.console.level=DEBUG + quarkus.log.file.level=DEBUG + quarkus.log.level=DEBUG + quarkus.mongodb.connection-string=mongodb://nessie-mongodb.default.svc.cluster.local:27017/nessie + quarkus.mongodb.database=nessie + quarkus.mongodb.credentials.username=mongodb + quarkus.oidc.auth-server-url=http://keycloak:8080/auth/realms/nessie + quarkus.oidc.client-id=quarkus-app + quarkus.otel.exporter.otlp.traces.endpoint=https://otlp-collector:4317 + quarkus.otel.resource.attributes=foo=bar,service.name=nessie-test + quarkus.otel.traces.sampler=parentbased_traceidratio + quarkus.otel.traces.sampler.arg=0.5d diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/deployment.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/deployment.yaml new file mode 100644 index 00000000000..1017dc0e5be --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/deployment.yaml @@ -0,0 +1,126 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + # replicas not set because of HPA + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + foo: bar + annotations: + foo: bar + projectnessie.org/config-checksum: sha256:c10d2d8d7199f0afcbc6e0cef2e5ca5af031724ab6878f32e3152f0d2aff7350 + spec: + securityContext: + fsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" + containers: + - name: nessie + image: projectnessie/nessie:1.2.3 + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: QUARKUS_PROFILE + value: "prod" + - name: quarkus.mongodb.credentials.password + valueFrom: + secretKeyRef: + name: nessie-db-credentials + key: password + livenessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 2 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 10 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 3 + periodSeconds: 45 + successThreshold: 1 + timeoutSeconds: 10 + serviceAccountName: nessie-test + volumes: + - name: nessie-config + configMap: + name: nessie-test +status: + observedGeneration: 1 + replicas: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + conditions: + - type: Available + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:21Z' + reason: MinimumReplicasAvailable + message: Deployment has minimum availability. + - type: Progressing + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:18Z' + reason: NewReplicaSetAvailable + message: ReplicaSet "nessie-test-abcdefg" has successfully progressed. diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/ingress.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/ingress.yaml new file mode 100644 index 00000000000..f6db8e74a81 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/ingress.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + kubernetes.io/ingress.class: nginx +spec: + rules: + - host: nessie.example.com + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: nessie-test + port: + number: 19120 + tls: + - hosts: + - nessie.example.com + secretName: nessie-test-tls +status: + loadBalancer: + ingress: + - ip: 192.168.49.2 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/nessie.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/nessie.yaml new file mode 100644 index 00000000000..b51acd8ea64 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/nessie.yaml @@ -0,0 +1,112 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + size: 1 + logLevel: DEBUG + service: + type: ClusterIP + sessionAffinity: ClientIP + port: 19120 + labels: {} + annotations: {} + versionStore: + type: MongoDb + cache: + enabled: false + mongoDb: + connectionString: mongodb://nessie-mongodb.default.svc.cluster.local:27017/nessie + database: nessie + username: mongodb + password: + secret: nessie-db-credentials + key: password + ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: nginx + rules: + - host: nessie.example.com + paths: + - / + tls: + - secret: nessie-test-tls + hosts: + - nessie.example.com + authentication: + enabled: true + oidcAuthServerUrl: http://keycloak:8080/auth/realms/nessie + oidcClientId: quarkus-app + authorization: + enabled: true + rules: + allowViewingBranch: op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + telemetry: + enabled: true + endpoint: https://otlp-collector:4317 + sample: "0.5d" + attributes: + foo: "bar" + monitoring: + enabled: true + labels: + foo: bar + interval: 1s + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 2 + targetCpuUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + extraEnv: + - name: QUARKUS_PROFILE + value: "prod" + advancedConfig: + nessie.server.default-branch: my-branch + nessie.version.store.persist.repository-id: my-repository + quarkus: + log: + console.format: "%d{HH:mm:ss} %s%e%n" + category."org.projectnessie".level: "TRACE" + deployment: + image: + repository: projectnessie/nessie + tag: 1.2.3 + pullPolicy: Never + labels: + foo: bar + annotations: + foo: bar + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + serviceAccount: + create: true + annotations: + foo: bar + podSecurityContext: + fsGroup: 1000 + containerSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-account.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-account.yaml new file mode 100644 index 00000000000..3e6080e052d --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-account.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + foo: bar diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-mgmt.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-monitor.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-monitor.yaml new file mode 100644 index 00000000000..8852220e45b --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service-monitor.yaml @@ -0,0 +1,26 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + namespaceSelector: + matchNames: + - @namespace@ + endpoints: + - port: nessie-mgmt + scheme: http + path: /q/metrics + interval: 1s diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service.yaml new file mode 100644 index 00000000000..50e230895cf --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/mongo/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-server + protocol: TCP + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + type: ClusterIP + sessionAffinity: ClientIP diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/config-map.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/config-map.yaml new file mode 100644 index 00000000000..a0f66760065 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/config-map.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +data: + application.properties: | + nessie.server.authentication.enabled=true + nessie.server.authorization.enabled=true + nessie.server.authorization.rules.allowViewingBranch=op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + nessie.server.default-branch=my-branch + nessie.version.store.persist.cache-capacity-fraction-adjust-mb=256 + nessie.version.store.persist.cache-capacity-fraction-min-size-mb=64 + nessie.version.store.persist.cache-capacity-fraction-of-heap=0.7 + nessie.version.store.persist.cache-capacity-mb=1024 + nessie.version.store.persist.repository-id=my-repository + nessie.version.store.persist.rocks.database-path=/rocks-nessie + nessie.version.store.type=ROCKSDB + quarkus.log.category."org.projectnessie".level=TRACE + quarkus.log.console.format=%d{HH:mm:ss} %s%e%n + quarkus.log.console.level=DEBUG + quarkus.log.file.level=DEBUG + quarkus.log.level=DEBUG + quarkus.oidc.auth-server-url=http://keycloak:8080/auth/realms/nessie + quarkus.oidc.client-id=quarkus-app + quarkus.otel.exporter.otlp.traces.endpoint=https://otlp-collector:4317 + quarkus.otel.resource.attributes=foo=bar,service.name=nessie-test + quarkus.otel.traces.sampler=parentbased_traceidratio + quarkus.otel.traces.sampler.arg=0.5d diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/deployment.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/deployment.yaml new file mode 100644 index 00000000000..f64512a5fb7 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/deployment.yaml @@ -0,0 +1,126 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + template: + metadata: + labels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + foo: bar + annotations: + foo: bar + projectnessie.org/config-checksum: sha256:cabad0c51fce79e2526978acd5ed920a1efe1e36e10f4f6bf33358b1ae44ff39 + spec: + securityContext: + fsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" + containers: + - name: nessie + image: projectnessie/nessie:1.2.3 + imagePullPolicy: Never + ports: + - name: nessie-server + containerPort: 19120 + protocol: TCP + - name: nessie-mgmt + containerPort: 9000 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: nessie-config + mountPath: /deployments/config/application.properties + subPath: application.properties + - mountPath: /rocks-nessie + name: rocks-storage + env: + - name: JAVA_OPTS_APPEND + value: "-XX:InitialRAMPercentage=70.0 -XX:MaxRAMPercentage=70.0" + - name: QUARKUS_PROFILE + value: "prod" + livenessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/live + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 2 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 10 + readinessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/ready + port: nessie-mgmt + scheme: HTTP + initialDelaySeconds: 3 + periodSeconds: 45 + successThreshold: 1 + timeoutSeconds: 10 + volumes: + - name: nessie-config + configMap: + name: nessie-test + - name: rocks-storage + persistentVolumeClaim: + claimName: nessie-test + serviceAccountName: nessie-test-custom-service-account +status: + observedGeneration: 1 + replicas: 1 + updatedReplicas: 1 + readyReplicas: 1 + availableReplicas: 1 + conditions: + - type: Available + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:21Z' + reason: MinimumReplicasAvailable + message: Deployment has minimum availability. + - type: Progressing + status: 'True' + lastUpdateTime: '2024-01-22T14:16:21Z' + lastTransitionTime: '2024-01-22T14:16:18Z' + reason: NewReplicaSetAvailable + message: ReplicaSet "nessie-test-abcdefg" has successfully progressed. diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/ingress.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/ingress.yaml new file mode 100644 index 00000000000..b99d4875371 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/ingress.yaml @@ -0,0 +1,32 @@ +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + kubernetes.io/ingress.class: nginx +spec: + rules: + - host: nessie.example.com + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + serviceName: nessie-test + servicePort: 19120 + tls: + - hosts: + - nessie.example.com + secretName: nessie-test-tls +status: + loadBalancer: + ingress: + - ip: 192.168.49.2 diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/nessie.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/nessie.yaml new file mode 100644 index 00000000000..1486943aaa3 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/nessie.yaml @@ -0,0 +1,109 @@ +apiVersion: nessie.projectnessie.org/v1alpha1 +kind: Nessie +metadata: + name: nessie-test +spec: + size: 1 + logLevel: DEBUG + service: + type: ClusterIP + sessionAffinity: ClientIP + port: 19120 + labels: {} + annotations: {} + versionStore: + type: RocksDb + rocksDb: + storageClassName: standard + storageSize: 1Gi + selectorLabels: + foo: bar + ingress: + enabled: true + annotations: + kubernetes.io/ingress.class: nginx + rules: + - host: nessie.example.com + paths: + - / + tls: + - secret: nessie-test-tls + hosts: + - nessie.example.com + authentication: + enabled: true + oidcAuthServerUrl: http://keycloak:8080/auth/realms/nessie + oidcClientId: quarkus-app + authorization: + enabled: true + rules: + allowViewingBranch: op=='VIEW_REFERENCE' && role.startsWith('test_user') && ref.startsWith('allowedBranch') + telemetry: + enabled: true + endpoint: https://otlp-collector:4317 + sample: "0.5d" + attributes: + foo: "bar" + monitoring: + enabled: true + labels: + foo: bar + interval: 1s + autoscaling: + enabled: false + extraEnv: + - name: QUARKUS_PROFILE + value: "prod" + advancedConfig: + nessie.server.default-branch: my-branch + nessie.version.store.persist.repository-id: my-repository + nessie.version.store.persist.cache-capacity-mb: 1024 + nessie.version.store.persist.cache-capacity-fraction-of-heap: 0.7 + nessie.version.store.persist.cache-capacity-fraction-adjust-mb: 256 + nessie.version.store.persist.cache-capacity-fraction-min-size-mb: 64 + quarkus: + log: + console.format: "%d{HH:mm:ss} %s%e%n" + category."org.projectnessie".level: "TRACE" + deployment: + image: + repository: projectnessie/nessie + tag: 1.2.3 + pullPolicy: Never + labels: + foo: bar + annotations: + foo: bar + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi + serviceAccount: + create: true + name: nessie-test-custom-service-account + annotations: + foo: bar + podSecurityContext: + fsGroup: 1000 + containerSecurityContext: + runAsUser: 1000 + runAsGroup: 1000 + nodeSelector: + foo: bar + tolerations: + - key: "key" + operator: "Equal" + value: "value" + effect: "NoSchedule" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "key" + operator: "In" + values: + - "value" diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/pvc.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/pvc.yaml new file mode 100644 index 00000000000..300a8247f2c --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/pvc.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + selector: + matchLabels: + foo: bar +status: + phase: Bound diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-account.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-account.yaml new file mode 100644 index 00000000000..0818154f142 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-account.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nessie-test-custom-service-account + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + annotations: + foo: bar diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-mgmt.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-mgmt.yaml new file mode 100644 index 00000000000..b003349a1f0 --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-mgmt.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-mgmt + protocol: TCP + port: 9000 + targetPort: 9000 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + clusterIP: None + publishNotReadyAddresses: true diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-monitor.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-monitor.yaml new file mode 100644 index 00000000000..8852220e45b --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service-monitor.yaml @@ -0,0 +1,26 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" + foo: bar +spec: + selector: + matchLabels: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + namespaceSelector: + matchNames: + - @namespace@ + endpoints: + - port: nessie-mgmt + scheme: http + path: /q/metrics + interval: 1s diff --git a/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service.yaml b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service.yaml new file mode 100644 index 00000000000..50e230895cf --- /dev/null +++ b/operator/src/test/resources/org/projectnessie/operator/tests/fixtures/rocks/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: nessie-test + labels: + app.kubernetes.io/component: nessie + app.kubernetes.io/instance: nessie-test + app.kubernetes.io/managed-by: nessie-controller + app.kubernetes.io/name: nessie + app.kubernetes.io/part-of: nessie + # noinspection KubernetesUnknownValues + app.kubernetes.io/version: "@projectVersion@" +spec: + ports: + - name: nessie-server + protocol: TCP + port: 19120 + targetPort: 19120 + selector: + app.kubernetes.io/name: nessie + app.kubernetes.io/instance: nessie-test + type: ClusterIP + sessionAffinity: ClientIP diff --git a/operator/src/testFixtures/java/org/projectnessie/operator/reconciler/AbstractReconcilerTests.java b/operator/src/testFixtures/java/org/projectnessie/operator/reconciler/AbstractReconcilerTests.java new file mode 100644 index 00000000000..19427d457df --- /dev/null +++ b/operator/src/testFixtures/java/org/projectnessie/operator/reconciler/AbstractReconcilerTests.java @@ -0,0 +1,321 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Event; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.api.model.Namespace; +import io.fabric8.kubernetes.api.model.NamespaceBuilder; +import io.fabric8.kubernetes.api.model.PersistentVolumeClaim; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceAccount; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.autoscaling.v2.HorizontalPodAutoscaler; +import io.fabric8.kubernetes.api.model.networking.v1.Ingress; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.openshift.api.model.monitoring.v1.ServiceMonitor; +import io.fabric8.openshift.client.OpenShiftClient; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.extractor.Extractors; +import org.awaitility.core.ConditionTimeoutException; +import org.awaitility.core.ThrowingRunnable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.projectnessie.operator.events.EventReason; +import org.projectnessie.operator.utils.EventUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class AbstractReconcilerTests { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractReconcilerTests.class); + private static final AtomicInteger COUNTER = new AtomicInteger(); + + protected OpenShiftClient client; + protected T primary; + protected Namespace namespace; + + @BeforeEach + void createTestNamespace() { + String namespaceName = "test-" + COUNTER.incrementAndGet(); + namespace = + new NamespaceBuilder().withNewMetadata().withName(namespaceName).endMetadata().build(); + client.namespaces().resource(namespace).create(); + } + + @Test + void createAndDelete() { + primary = newPrimary(); + primary.getMetadata().setNamespace(namespace.getMetadata().getName()); + LOGGER.info( + "Creating {} {} in namespace {}", + primary.getSingular(), + primary.getMetadata().getName(), + namespace.getMetadata().getName()); + primary = client.resource(primary).create(); + awaitUntilAsserted(this::assertResourcesCreated, "Failed to assert resources created"); + waitForPrimaryReady(); + setUpFunctionalTest(); + LOGGER.info( + "Testing {} {} in namespace {}", + primary.getSingular(), + primary.getMetadata().getName(), + namespace.getMetadata().getName()); + awaitUntilAsserted(this::functionalTest, "Functional test failed"); + LOGGER.info( + "Deleting {} {} in namespace {}", + primary.getSingular(), + primary.getMetadata().getName(), + namespace.getMetadata().getName()); + client.resource(primary).delete(); + awaitUntilAsserted(this::assertResourcesDeleted, "Failed to assert resources deleted"); + } + + protected abstract Duration pollInterval(); + + protected abstract Duration timeout(); + + protected abstract T newPrimary(); + + protected void refreshPrimary() { + primary = client.resource(primary).get(); + } + + protected void waitForPrimaryReady() {} + + protected abstract void assertResourcesCreated() throws Exception; + + protected abstract void setUpFunctionalTest(); + + protected abstract void functionalTest() throws Exception; + + protected abstract void assertResourcesDeleted() throws Exception; + + protected R get( + MixedOperation> resources, String name) { + return resources.inNamespace(namespace.getMetadata().getName()).withName(name).get(); + } + + protected > List list( + MixedOperation> resources) { + return resources.inNamespace(namespace.getMetadata().getName()).list().getItems(); + } + + protected R load( + MixedOperation> op, String classpathResource) { + return loadResource(op, classpathResource).item(); + } + + protected R create( + MixedOperation> op, String classpathResource) { + return loadResource(op, classpathResource).create(); + } + + private Resource loadResource( + MixedOperation> op, String classpathResource) { + Resource resource = op.load(openStream(classpathResource)); + resource.item().getMetadata().setNamespace(namespace.getMetadata().getName()); + return resource; + } + + private InputStream openStream(String classpathResource) { + try (InputStream in = getClass().getResourceAsStream(classpathResource)) { + String contents = + new String(Objects.requireNonNull(in).readAllBytes(), StandardCharsets.UTF_8); + contents = contents.replaceAll("@namespace@", namespace.getMetadata().getName()); + return new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected void checkServiceAccount(ServiceAccount expected, ServiceAccount actual) { + assertThat(actual).isNotNull(); + checkMeta(expected, actual); + } + + protected void checkConfigMap(ConfigMap expected, ConfigMap actual, Object... overrides) { + assertThat(actual).isNotNull(); + checkMeta(expected, actual); + assertThat(actual.getData()).hasSize(1).containsKey("application.properties"); + Properties actualProperties = new Properties(); + Properties expectedProperties = new Properties(); + try { + actualProperties.load( + new ByteArrayInputStream( + actual.getData().get("application.properties").getBytes(StandardCharsets.UTF_8))); + expectedProperties.load( + new ByteArrayInputStream( + expected.getData().get("application.properties").getBytes(StandardCharsets.UTF_8))); + } catch (IOException e) { + throw new RuntimeException(e); + } + for (int i = 0; i < overrides.length; i++) { + expectedProperties.setProperty(overrides[i].toString(), overrides[++i].toString()); + } + assertThat(actualProperties).isEqualTo(expectedProperties); + assertThat(actual.getBinaryData()).isNullOrEmpty(); + } + + protected void checkPvc(PersistentVolumeClaim expected, PersistentVolumeClaim actual) { + checkDependent(expected, actual, "volumeName"); + } + + protected void checkDeployment(Deployment expected, Deployment actual) { + checkDependent(expected, actual); + } + + protected void checkService(Service expected, Service actual) { + checkDependent(expected, actual, "clusterIP", "clusterIPs", "ipFamilies"); + } + + protected void checkIngress(Ingress expected, Ingress actual) { + checkDependent(expected, actual); + } + + protected void checkIngress( + io.fabric8.kubernetes.api.model.networking.v1beta1.Ingress expected, + io.fabric8.kubernetes.api.model.networking.v1beta1.Ingress actual) { + checkDependent(expected, actual); + } + + protected void checkServiceMonitor(ServiceMonitor expected, ServiceMonitor actual) { + checkDependent(expected, actual); + } + + protected void checkAutoscaler(HorizontalPodAutoscaler expected, HorizontalPodAutoscaler actual) { + checkDependent(expected, actual); + } + + protected void checkAutoscaler( + io.fabric8.kubernetes.api.model.autoscaling.v2beta2.HorizontalPodAutoscaler expected, + io.fabric8.kubernetes.api.model.autoscaling.v2beta2.HorizontalPodAutoscaler actual) { + checkDependent(expected, actual); + } + + protected void checkAutoscaler( + io.fabric8.kubernetes.api.model.autoscaling.v2beta1.HorizontalPodAutoscaler expected, + io.fabric8.kubernetes.api.model.autoscaling.v2beta1.HorizontalPodAutoscaler actual) { + checkDependent(expected, actual); + } + + protected void checkDependent( + HasMetadata expected, HasMetadata actual, String... ignoredSpecFields) { + assertThat(actual).isNotNull(); + checkMeta(expected, actual); + checkSpec(expected, actual, ignoredSpecFields); + } + + protected void checkEvents(EventReason... reasons) { + for (EventReason reason : reasons) { + Event event = get(client.v1().events(), EventUtils.eventName(primary, reason)); + assertThat(event).as("Expecting event with reason %s to exist", reason).isNotNull(); + assertThat(event.getType()).isEqualTo(reason.type().name()); + } + } + + protected void checkEvent(EventReason reason, String message) { + Event event = get(client.v1().events(), EventUtils.eventName(primary, reason)); + assertThat(event).isNotNull(); + assertThat(event.getType()).isEqualTo(reason.type().name()); + assertThat(event.getMessage()).isEqualTo(message); + } + + protected void checkNotCreated( + MixedOperation, ?> operation) { + try { + assertThat(operation.inNamespace(namespace.getMetadata().getName()).list().getItems()) + .isNullOrEmpty(); + } catch (KubernetesClientException e) { + // The resource doesn't even exist in the cluster + assertThat(e.getStatus().getCode()).isEqualTo(404); + } + } + + protected void checkNotCreated( + MixedOperation, ?> operation, String name) { + try { + assertThat(operation.inNamespace(namespace.getMetadata().getName()).withName(name).get()) + .isNull(); + } catch (KubernetesClientException e) { + // The resource doesn't even exist in the cluster + assertThat(e.getStatus().getCode()).isEqualTo(404); + } + } + + private void awaitUntilAsserted(ThrowingRunnable code, String message) { + try { + await() + .pollInterval(pollInterval()) + .atMost(timeout()) + .untilAsserted( + () -> { + try { + code.run(); + } catch (AssertionError t) { + throw t; + } catch (Throwable t) { + throw new AssertionError(message, t); + } + }); + } catch (ConditionTimeoutException e) { + LOGGER.error(message, e.getCause()); + // clear interrupt flag + LOGGER.error("Interrupt status: {}", Thread.interrupted()); + dumpNamespace(); + fail(message, e.getCause()); + } + } + + protected void dumpNamespace() {} + + private static void checkMeta(HasMetadata expected, HasMetadata actual) { + assertThat(actual.getMetadata()).isNotNull(); + assertThat(actual.getMetadata().getLabels()) + .containsAllEntriesOf(expected.getMetadata().getLabels()); + assertThat(actual.getMetadata().getAnnotations()) + .containsAllEntriesOf(expected.getMetadata().getAnnotations()); + } + + private static void checkSpec(HasMetadata expected, HasMetadata actual, String... ignoredFields) { + assertThat(actual) + .extracting("spec") + .usingRecursiveComparison() + .ignoringExpectedNullFields() + .ignoringCollectionOrder() + .ignoringFields(ignoredFields) + .isNotNull() + .isEqualTo(Extractors.byName("spec").apply(expected)); + } +} diff --git a/tools/aggregated-license-report/build.gradle.kts b/tools/aggregated-license-report/build.gradle.kts index 10174fa0b99..730510d3404 100644 --- a/tools/aggregated-license-report/build.gradle.kts +++ b/tools/aggregated-license-report/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { licenseReports(nessieProject("nessie-gc-tool", "licenseReports")) licenseReports(nessieProject("nessie-content-generator", "licenseReports")) licenseReports(nessieProject("nessie-cli", "licenseReports")) + licenseReports(nessieProject("nessie-operator", "licenseReports")) notice(nessieProject("nessie-notice")) rootProject.subprojects .filter { p -> p.name.startsWith("nessie-spark-extensions-3") }