From 934c2cef7e3b7f946dc9d69f6f9103e20fa18014 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 | 64 ++- LICENSE | 28 + 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 | 151 +++++ 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 | 108 ++++ ...tractNessieReconcilerIntegrationTests.java | 174 ++++++ .../nessie/ITNessieReconcilerBigTable.java | 120 ++++ .../nessie/ITNessieReconcilerCassandra.java | 107 ++++ .../nessie/ITNessieReconcilerDynamo.java | 153 ++++++ .../nessie/ITNessieReconcilerJdbc.java | 125 +++++ .../nessie/ITNessieReconcilerMongo.java | 100 ++++ .../nessie/ITNessieReconcilerRocks.java | 110 ++++ .../AbstractContainerLifecycleManager.java | 95 ++++ .../BigTableContainerLifecycleManager.java | 58 ++ .../CassandraContainerLifecycleManager.java | 63 +++ .../DynamoContainerLifecycleManager.java | 52 ++ .../K3sContainerLifecycleManager.java | 520 ++++++++++++++++++ .../KeycloakContainerLifecycleManager.java | 72 +++ .../MongoContainerLifecycleManager.java | 66 +++ .../PostgresContainerLifecycleManager.java | 52 ++ .../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 | 3 + .../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 | 59 ++ 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, 12679 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 a7f05b684a6..df74df5eb7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,14 +80,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 @@ -124,7 +125,7 @@ jobs: uses: ./.github/actions/ci-incr-build-cache-prepare - 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@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4 @@ -170,6 +171,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: | @@ -230,6 +238,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) \ @@ -477,6 +486,52 @@ jobs: with: job-name: 'int-test-quarkus-events' + int-test-quarkus-operator: + name: CI intTest Quarkus Operator + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Setup runner + uses: ./.github/actions/setup-runner + - name: Setup Java, Gradle + uses: ./.github/actions/dev-tool-java + + - name: Prepare Gradle build cache + uses: ./.github/actions/ci-incr-build-cache-prepare + + - 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' + determine-jobs: name: CI Determine jobs runs-on: ubuntu-22.04 @@ -998,6 +1053,7 @@ jobs: - int-test-quarkus-server - int-test-quarkus-tool - int-test-quarkus-events + - int-test-quarkus-operator steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup runner diff --git a/LICENSE b/LICENSE index a63a380d9a1..88fdf74a587 100755 --- a/LICENSE +++ b/LICENSE @@ -403,6 +403,34 @@ 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. +--- +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 b3df29891ea..2e40a0bea10 100644 --- a/bom/build.gradle.kts +++ b/bom/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { api(project(":nessie-minio-testcontainer")) api(project(":nessie-nessie-testcontainer")) api(project(":nessie-network-tools")) + api(project(":nessie-operator")) api(project(":nessie-quarkus-auth")) api(project(":nessie-quarkus-catalog")) api(project(":nessie-quarkus-distcache")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03c6a135985..993d347b73b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -124,6 +124,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.5.0" } rocksdb-jni = { module = "org.rocksdb:rocksdbjni", version = "9.5.2" } scala-library-v212 = { module = "org.scala-lang:scala-library", version = { strictly = "[2.12, 2.13[", prefer = "2.12.19" }} @@ -140,6 +141,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.3"}} 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.20.1" } 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 ff6045a0d16..43fb5e4159f 100644 --- a/gradle/projects.main.properties +++ b/gradle/projects.main.properties @@ -32,6 +32,7 @@ nessie-keycloak-testcontainer=testing/keycloak-container nessie-nessie-testcontainer=testing/nessie-container nessie-network-tools=tools/network nessie-object-storage-mock=testing/object-storage-mock +nessie-operator=operator nessie-quarkus=servers/quarkus-server nessie-quarkus-auth=servers/quarkus-auth nessie-quarkus-catalog=servers/quarkus-catalog 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..6f349934658 --- /dev/null +++ b/operator/build.gradle.kts @@ -0,0 +1,151 @@ +/* + * 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-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) + intTestCompileOnly(libs.immutables.value.annotations) +} + +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("quarkusDependenciesBuild").configure { dependsOn("processJandexIndex") } + +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..0c055afb225 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/AbstractReconcilerIntegrationTests.java @@ -0,0 +1,108 @@ +/* + * 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.Node; +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; +import org.testcontainers.containers.Container.ExecResult; + +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(3); + } + + @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 dumpDiagnostics() { + LOGGER.error("API server logs:\n{}", kubectl.apiServerLogs()); + ExecResult result = kubectl.exec("top", "pod", "--all-namespaces"); + LOGGER.error("Top pods:\n{}", result.getStdout()); + result = kubectl.exec("top", "node"); + LOGGER.error("Top nodes:\n{}", result.getStdout()); + if (client != null) { + LOGGER.error("Dumping nodes"); + for (Node node : client.nodes().list().getItems()) { + LOGGER.error("{}", client.getKubernetesSerialization().asYaml(node)); + } + LOGGER.error("Dumping pods in 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..88c47a07c7c --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerBigTable.java @@ -0,0 +1,120 @@ +/* + * 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.ResourceArg; +import io.quarkus.test.common.WithTestResource; +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 +@WithTestResource(value = BigTableContainerLifecycleManager.class, parallel = true) +@WithTestResource( + value = K3sContainerLifecycleManager.class, + parallel = 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..a9781828674 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerCassandra.java @@ -0,0 +1,107 @@ +/* + * 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.ResourceArg; +import io.quarkus.test.common.WithTestResource; +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 +@WithTestResource(value = CassandraContainerLifecycleManager.class, parallel = true) +@WithTestResource( + value = K3sContainerLifecycleManager.class, + parallel = 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..e85c3763126 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerDynamo.java @@ -0,0 +1,153 @@ +/* + * 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.ResourceArg; +import io.quarkus.test.common.WithTestResource; +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 +@WithTestResource(value = DynamoContainerLifecycleManager.class, parallel = true) +@WithTestResource( + value = K3sContainerLifecycleManager.class, + parallel = 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..f622a5e6b71 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerJdbc.java @@ -0,0 +1,125 @@ +/* + * 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.WithTestResource; +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 +@WithTestResource(value = PostgresContainerLifecycleManager.class, parallel = true) +@WithTestResource(value = K3sContainerLifecycleManager.class, parallel = 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..ccb9ee20989 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerMongo.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 Dremio + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.operator.reconciler.nessie; + +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.WithTestResource; +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 +@WithTestResource(value = MongoContainerLifecycleManager.class, parallel = true) +@WithTestResource(value = K3sContainerLifecycleManager.class, parallel = 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..cfc5d9c9553 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/reconciler/nessie/ITNessieReconcilerRocks.java @@ -0,0 +1,110 @@ +/* + * 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.WithTestResource; +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 +@WithTestResource(value = KeycloakContainerLifecycleManager.class, parallel = true) +@WithTestResource(value = K3sContainerLifecycleManager.class, parallel = 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..8c344f29a53 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/AbstractContainerLifecycleManager.java @@ -0,0 +1,95 @@ +/* + * 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 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; + + private String inDockerIpAddress; + + protected AbstractContainerLifecycleManager() {} + + @Override + public Map start() { + inDockerIpAddress = null; + Logger logger = LoggerFactory.getLogger(getClass()); + container = createContainer(); + container + .withNetwork(Network.SHARED) + .withLogConsumer(new Slf4jLogConsumer(logger).withPrefix(container.getDockerImageName())) + .withStartupAttempts(3); + container.start(); + 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. + */ + protected String getInDockerIpAddress() { + if (inDockerIpAddress == null) { + inDockerIpAddress = resolveInDockerIpAddress(); + Objects.requireNonNull( + inDockerIpAddress, "Failed to resolve container's in-docker IP address"); + } + return inDockerIpAddress; + } + + private String resolveInDockerIpAddress() { + 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..04d875e472b --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/BigTableContainerLifecycleManager.java @@ -0,0 +1,58 @@ +/* + * 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( + getInDockerIpAddress(), + 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..753c9cd7977 --- /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 = getInDockerIpAddress() + ":" + 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..07b3ccfc109 --- /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", getInDockerIpAddress(), 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..749b8ba40b5 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/K3sContainerLifecycleManager.java @@ -0,0 +1,520 @@ +/* + * 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<>(); + List commandParts = new ArrayList<>(); + commandParts.add("server"); + commandParts.add("--tls-san=" + container.getHost()); + // Mitigate eviction issues in CI by setting eviction thresholds for nodefs very low + commandParts.add("--kubelet-arg=eviction-hard=nodefs.available<1%,nodefs.inodesFree<1%"); + // Enable rootless containers + commandParts.add("--kubelet-arg=feature-gates=KubeletInUserNamespace=true"); + 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"); + exposedPorts.add(NESSIE_INGRESS_PORT); + } else { + commandParts.add("--disable=traefik"); + exposedPorts.add(NESSIE_NODE_PORT); + } + if (telemetry) { + container.withCopyToContainer( + Transferable.of(COLLECTOR_HELM_CHART), + "/var/lib/rancher/k3s/server/manifests/otel-collector.yaml"); + } + container.setCommand(commandParts.toArray(new String[0])); + 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() { + try { + if (k8sClient != null) { + k8sClient.close(); + } + } finally { + 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 String apiServerLogs() { + return container.getLogs(); + } + } + + 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..262761805db --- /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", getInDockerIpAddress())); + 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..1001e4cade9 --- /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", getInDockerIpAddress(), 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..1252078ce65 --- /dev/null +++ b/operator/src/intTest/java/org/projectnessie/operator/testinfra/PostgresContainerLifecycleManager.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 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(getInDockerIpAddress(), 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..9f1c319b2a2 --- /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:484.0.0-debian_component_based 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..e6a55c3e597 --- /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 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..56f82b9f27a --- /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.2 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..63d902dada4 --- /dev/null +++ b/operator/src/intTest/resources/org/projectnessie/operator/testinfra/Dockerfile-k3s-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/rancher/k3s:v1.30.2-k3s2 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..ba9f2d26671 --- /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 docker.io/mongo:7.0.12 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..937bbd018a5 --- /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 docker.io/postgres:16.3 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..23e5e4c926f --- /dev/null +++ b/operator/src/main/resources/application.properties @@ -0,0 +1,59 @@ +# 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=${quarkus.application.version} +quarkus.kubernetes.namespace=nessie-operator +quarkus.kubernetes.image-pull-policy=IfNotPresent +quarkus.kubernetes.prometheus.generate-service-monitor=true +quarkus.kubernetes.prometheus.annotations=true + +# 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..16c66432a9e --- /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()); + dumpDiagnostics(); + fail(message, e.getCause()); + } + } + + protected void dumpDiagnostics() {} + + 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 326ccde6ac0..4d51cf6b646 100644 --- a/tools/aggregated-license-report/build.gradle.kts +++ b/tools/aggregated-license-report/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { licenseReports(nessieProject("nessie-gc-tool", "licenseReports")) licenseReports(nessieProject("nessie-content-generator", "licenseReports")) licenseReports(nessieProject("nessie-cli", "licenseReports")) + licenseReports(nessieProject("nessie-operator", "licenseReports")) rootProject.subprojects .filter { p -> p.name.startsWith("nessie-spark-extensions-3") } .forEach { p -> licenseReports(nessieProject(p.path.substring(1), "licenseReports")) }