From 8cdc6f1db2283b28b37bc0ad0aae8e699215c7e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Gurhem?= Date: Fri, 4 Oct 2024 17:55:30 +0200 Subject: [PATCH] feat: add static init for instances of Partition and Authentication --- .docs/content/1.concepts/3.authentication.md | 88 ++--- .github/workflows/build.yml | 49 +-- .../src/Common/IMongoDataModelMapping.cs | 36 ++ .../src/Common/MongoCollectionProvider.cs | 103 ++++-- .../DataModel/Auth/AuthDataModelMapping.cs | 16 + .../DataModel/Auth/RoleDataModelMapping.cs | 16 + .../DataModel/Auth/UserDataModelMapping.cs | 16 + .../DataModel/PartitionDataModelMapping.cs | 16 + Adaptors/MongoDB/tests/IndexTest.cs | 6 + Adaptors/MongoDB/tests/InjectionTests.cs | 5 + .../MongoDB/tests/MongoDatabaseProvider.cs | 8 +- .../MongoDB/tests/SessionProviderTests.cs | 6 + .../Options/Database/Authentication.cs | 46 +++ .../Injection/Options/Database/Certificate.cs | 61 ++++ .../Options/Database/InitDatabase.cs | 105 ++++++ .../Injection/Options/Database/Partition.cs | 86 +++++ .../Options/Database/Partitioning.cs | 36 ++ Common/src/Injection/Options/Database/Role.cs | 68 ++++ Common/src/Injection/Options/Database/User.cs | 68 ++++ Common/src/Injection/Options/InitServices.cs | 62 ++++ Common/src/Storage/PartitionData.cs | 2 +- Common/tests/Helpers/TestDatabaseProvider.cs | 6 + Common/tests/Helpers/TestPollsterProvider.cs | 5 + .../tests/Helpers/TestTaskHandlerProvider.cs | 5 + Common/tests/StaticInitTests.cs | 316 ++++++++++++++++++ Common/tests/Submitter/SubmitterTests.cs | 5 + .../TestBase/AuthenticationTableTestBase.cs | 46 +++ .../tests/TestBase/PartitionTableTestBase.cs | 50 +++ Compute/PollingAgent/src/Program.cs | 5 + Control/Metrics/src/Program.cs | 5 + Control/PartitionMetrics/src/Program.cs | 5 + Control/Submitter/src/Program.cs | 11 + justfile | 2 + terraform/locals.tf | 13 +- terraform/main.tf | 5 +- terraform/modules/compute_plane/inputs.tf | 4 + terraform/modules/compute_plane/locals.tf | 1 + .../modules/monitoring/metrics/inputs.tf | 4 + .../modules/monitoring/metrics/locals.tf | 5 +- terraform/modules/monitoring/metrics/main.tf | 2 +- .../monitoring/partition_metrics/inputs.tf | 4 + .../monitoring/partition_metrics/locals.tf | 5 +- .../monitoring/partition_metrics/main.tf | 2 +- .../modules/storage/database/mongo/inputs.tf | 4 - .../modules/storage/database/mongo/main.tf | 8 - .../modules/storage/database/mongo/outputs.tf | 2 +- terraform/modules/submitter/inputs.tf | 4 + terraform/modules/submitter/locals.tf | 9 +- terraform/modules/submitter/main.tf | 29 +- terraform/variables.tf | 7 +- 50 files changed, 1347 insertions(+), 121 deletions(-) create mode 100644 Common/src/Injection/Options/Database/Authentication.cs create mode 100644 Common/src/Injection/Options/Database/Certificate.cs create mode 100644 Common/src/Injection/Options/Database/InitDatabase.cs create mode 100644 Common/src/Injection/Options/Database/Partition.cs create mode 100644 Common/src/Injection/Options/Database/Partitioning.cs create mode 100644 Common/src/Injection/Options/Database/Role.cs create mode 100644 Common/src/Injection/Options/Database/User.cs create mode 100644 Common/src/Injection/Options/InitServices.cs create mode 100644 Common/tests/StaticInitTests.cs diff --git a/.docs/content/1.concepts/3.authentication.md b/.docs/content/1.concepts/3.authentication.md index 20e4aad77..ba616b204 100644 --- a/.docs/content/1.concepts/3.authentication.md +++ b/.docs/content/1.concepts/3.authentication.md @@ -35,7 +35,7 @@ When an user sends a request to an endpoint, they need to be authenticated, and ## Request authorization flowchart - +```mermaid flowchart TB User([User Certificate]) OK(OK) @@ -56,19 +56,11 @@ flowchart TB CheckPerm --> |No|DENIED CheckPerm --> |Yes|OK Cache --> |Yes|CheckPerm - +``` ## User administration -Users, roles, permissions and certificates are stored and managed in a database. Administrators in charge of handling user permissions can refer to this section to manage user permissions. - -### Authentication database deployments - -The following database deployments can be used to handle authentication data : - -- Internal MongoDB -- External MongoDB (Currently not supported) -- External SQL (Currently not supported) +Users, roles, permissions and certificates are stored and managed by ArmoniK via environment variables provided to the control plane and compute plane. Administrators in charge of handling user permissions can refer to this section to manage user permissions. ### Populating the internal MongoDB when deploying ArmoniK @@ -76,65 +68,81 @@ The following database deployments can be used to handle authentication data : You can define the users' roles and certificates using a json configuration during deployment. Check the [ArmoniK Authentication Configuration Guide](https://github.com/aneoconsulting/ArmoniK/blob/main/.docs/content/2.guide/1.how-to/how-to-configure-authentication.md) for more details. -### Using MongoDB directly - - -> **NOTE :** Using a MongoDB instance different from the one used in the rest of ArmoniK is currently not supported -> **NOTE :** If the deployment used is the internal MongoDB, to send commands to the database you need to use a script like the one available [here](https://github.com/aneoconsulting/ArmoniK/blob/main/tools/access-mongo-as-user.sh) to connect to the database as a user. Another script to connect to the database as admin is available [here](https://github.com/aneoconsulting/ArmoniK/blob/main/tools/access-mongo-as-admin.sh). -> **NOTE :** This method is available to anyone having access to the deployed cluster's secrets and an access to the MongoDB host and port. **Use it at your own risk**. +### Using ArmoniK environment variables -In order to function properly, the MongoDB database needs to have the following collections: +In order to function properly, the authentication needs to have the following collections: -- AuthData +- List of [Certificate](../../../Common/src/Injection/Options/Database/Certificate.cs) - Handles the association between certificates and users - Requires the following fields: - - UserId : _id field of the UserData object associated with this certificate + - User : Name field of the User object associated with this certificate - CN : Certificate's Common name - Fingerprint : null if this entry should match all certificates with the given CN, otherwise, the certificate's fingerprint - The CN and Fingerprint fields form a unique compound index. -- UserData +- List of [User](../../../Common/src/Injection/Options/Database/User.cs) - Handles the association between a user and its roles - Requires the following fields - - Username : Unique user name + - Name : Unique user name - Roles : list of objectIds, each matching the _id field in RoleData of the roles given to the user -- RoleData +- List of [Role](../../../Common/src/Injection/Options/Database/User.cs) - Handles the association between a role and its permissions - Requires the following fields - - RoleName : Unique role name + - Name : Unique role name - Permissions : list of strings corresponding to the permissions of the role -#### Insert a role +These collections of object needs to be provided as JSON objects as detailled in the following sections. -To insert a role with the name "Role1" granting the permissions "Submitter:ListTasks" and "General:Impersonate:Role2", use the following command : +#### Environment variables base -```javascript -db.RoleData.insertOne({RoleName:"Role1", Permissions:["Submitter:ListTasks", "General:Impersonate:Role2"]}) +An InitServices options class was introduced to initialize services. +It contains two classes : Authentication and Partitionning to configure authentications and Partitions respectively. +Authentication has several list of strings as fields: UserCertificates, Roles and Users. +Those fields are JSON strings that are deserialized into corresponding objects that will be inserted into the database. + +These options can be configured as environment variables. +Lists should be converted into an environment variable per element with its index as shown below. + +For example, the environment variables for the first and second roles are: + +```bash +InitServices__Authentication__Roles__0= +InitServices__Authentication__Roles__1= ``` -See the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.insertOne/) to get the RoleId (_id field) of the inserted role from the command's response. +#### Specify roles -#### Insert a user +To specify a role with the name "Role1" granting the permissions "Submitter:ListTasks" and "General:Impersonate:Role2", use the following environment variable: -To insert a user with the name "User1" with the role "Role1", use the following command : +```bash +InitServices__Authentication__Roles__0='{"Name": "Role1", "Permissions": ["Submitter:ListTasks", "General:Impersonate:Role2"]}' +``` -```javascript -db.UserData.insertOne({User:"User1", Roles:["Role1"]}) +If you need a second role, you can add the following environment variable too: + +```bash +InitServices__Authentication__Roles__1='{"Name": "Role2", "Permissions": ["Submitter:ListTasks", "General:Impersonate:Role3"]}' ``` -See the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/method/db.collection.insertOne/) to get the UserId (_id field) of the inserted user from the command's response. +#### Specify users -#### Insert a certificate +To specify a user with the name "User1" with the role "Role1", use the following command : + +```bash +InitServices__Authentication__Users__0='{"Name": "User1", "Roles": ["Role1"]})' +``` -To insert a certificate with Common Name "CN1" and Fingerprint "FP1" associated with the user with UserId "62f4efe6d82645e26e09584f", use the following command : +#### Specify certificates + +To insert a certificate with Common Name "CN1" and Fingerprint "FP1" associated with the user with the User called "User1", use the following command : ```javascript -db.AuthData.insertOne({UserId:"62f4efe6d82645e26e09584f", CN:"CN1", Fingerprint:"FP1"}) +InitServices__Authentication__UserCertificates__0='{"User": "User1", "CN": "CN1", "Fingerprint": "FP1"}' ``` To insert an entry matching all certificates with Common Name "CN1" associated with user with UserId "62f4efe6d82645e26e09584f", use the following command : ```javascript -db.AuthData.insertOne({UserId:"62f4efe6d82645e26e09584f", CN:"CN1"}) +InitServices__Authentication__UserCertificates__0='{"User": "User1", "CN": "CN1"}' ``` #### Edit a user/role/certificate @@ -145,10 +153,6 @@ Refer to [this link](https://www.mongodb.com/docs/manual/reference/method/db.col Refer to [this link](https://www.mongodb.com/docs/manual/reference/method/db.collection.findOneAndDelete/) for the procedure to delete MongoDB entries. -### Using SQL directly - -Currently not implemented - ### Using the User Administration gRPC service Currently not implemented diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d3eca3dd..b34c26092 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -421,19 +421,28 @@ jobs: strategy: fail-fast: false matrix: - queue: - - activemq - - rabbitmq - - rabbitmq091 - - pubsub - - sqs - object: - - redis - - minio - log-level: - - Information - - Verbose - name: HtcMock ${{ matrix.queue }} ${{ matrix.object }} ${{ matrix.log-level }} + target: + - { queue: activemq, object: redis, log-level: Information, cinit: true } + - { queue: rabbitmq, object: redis, log-level: Information, cinit: true } + - { queue: rabbitmq091, object: redis, log-level: Information, cinit: true } + - { queue: pubsub, object: redis, log-level: Information, cinit: true } + - { queue: sqs, object: redis, log-level: Information, cinit: true } + + - { queue: activemq, object: redis, log-level: Verbose, cinit: true } + - { queue: rabbitmq, object: redis, log-level: Verbose, cinit: true } + - { queue: rabbitmq091, object: redis, log-level: Verbose, cinit: true } + - { queue: pubsub, object: redis, log-level: Verbose, cinit: true } + - { queue: sqs, object: redis, log-level: Verbose, cinit: true } + + - { queue: activemq, object: minio, log-level: Information, cinit: true } + - { queue: rabbitmq, object: minio, log-level: Information, cinit: true } + - { queue: rabbitmq091, object: minio, log-level: Information, cinit: true } + - { queue: pubsub, object: minio, log-level: Information, cinit: true } + - { queue: sqs, object: minio, log-level: Information, cinit: true } + + - { queue: activemq, object: redis, log-level: Information, cinit: false } + + name: HtcMock ${{ matrix.target.queue }} ${{ matrix.target.object }} ${{ matrix.target.log-level }} ${{ matrix.target.cinit }} steps: - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 @@ -455,7 +464,7 @@ jobs: - name: Deploy Core run: | MONITOR_PREFIX="monitor/deploy/" tools/retry.sh -w 30 -- tools/monitor.sh \ - just log_level=${{ matrix.log-level }} tag=${VERSION} queue=${{ matrix.queue }} object=${{ matrix.object }} worker=htcmock deploy + just log_level=${{ matrix.target.log-level }} tag=${VERSION} queue=${{ matrix.target.queue }} object=${{ matrix.target.object }} cinit=${{ matrix.target.cinit }} worker=htcmock deploy sleep 10 - name: Print And Time Metrics @@ -524,7 +533,7 @@ jobs: - name: Run HtcMock test 1000 tasks 1 level timeout-minutes: 3 - if: ${{ matrix.log-level != 'Verbose' }} + if: ${{ matrix.target.log-level != 'Verbose' }} run: | MONITOR_PREFIX="monitor/htcmock-1000-1/" tools/monitor.sh \ docker run --net armonik_network --rm \ @@ -539,7 +548,7 @@ jobs: - name: Run HtcMock test 1000 tasks 4 levels timeout-minutes: 3 - if: ${{ matrix.log-level != 'Verbose' }} + if: ${{ matrix.target.log-level != 'Verbose' }} run: | MONITOR_PREFIX="monitor/htcmock-1000-4/" tools/monitor.sh \ docker run --net armonik_network --rm \ @@ -563,8 +572,8 @@ jobs: run: | export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} - docker cp fluentd:/armonik-logs.json - | gzip -c | aws s3 cp - s3://${{ secrets.AWS_LOG_BUCKET_NAME }}/core-pipeline/${{ github.run_number }}/${{ github.run_attempt }}/htcmock-${{ matrix.queue }}-${{ matrix.object }}-${{ matrix.log-level }}.json.gz - tar -czf - monitor/ | aws s3 cp - s3://${{ secrets.AWS_LOG_BUCKET_NAME }}/core-pipeline/${{ github.run_number }}/${{ github.run_attempt }}/htcmock-${{ matrix.queue }}-${{ matrix.object }}-${{ matrix.log-level }}-monitor.tar.gz + docker cp fluentd:/armonik-logs.json - | gzip -c | aws s3 cp - s3://${{ secrets.AWS_LOG_BUCKET_NAME }}/core-pipeline/${{ github.run_number }}/${{ github.run_attempt }}/htcmock-${{ matrix.target.queue }}-${{ matrix.target.object }}-${{ matrix.target.log-level }}-${{ matrix.target.cinit }}.json.gz + tar -czf - monitor/ | aws s3 cp - s3://${{ secrets.AWS_LOG_BUCKET_NAME }}/core-pipeline/${{ github.run_number }}/${{ github.run_attempt }}/htcmock-${{ matrix.target.queue }}-${{ matrix.target.object }}-${{ matrix.target.log-level }}-${{ matrix.target.cinit }}-monitor.tar.gz - name: Collect docker container logs uses: jwalton/gh-docker-logs@2741064ab9d7af54b0b1ffb6076cf64c16f0220e # v2 @@ -576,14 +585,14 @@ jobs: run: | export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} - tar -cvf - ./container-logs | aws s3 cp - s3://${{ secrets.AWS_LOG_BUCKET_NAME }}/core-pipeline/${{ github.run_number }}/${{ github.run_attempt }}/htcmock-${{ matrix.queue }}-${{ matrix.object }}-${{ matrix.log-level }}-container-logs.tar.gz + tar -cvf - ./container-logs | aws s3 cp - s3://${{ secrets.AWS_LOG_BUCKET_NAME }}/core-pipeline/${{ github.run_number }}/${{ github.run_attempt }}/htcmock-${{ matrix.target.queue }}-${{ matrix.target.object }}-${{ matrix.target.log-level }}-${{ matrix.target.cinit }}-container-logs.tar.gz - name: Export and upload database if: always() run: | export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} bash tools/export_mongodb.sh - tar -cvf - *.json | aws s3 cp - s3://${{ secrets.AWS_LOG_BUCKET_NAME }}/core-pipeline/${{ github.run_number }}/${{ github.run_attempt }}/htcmock-${{ matrix.queue }}-${{ matrix.object }}-${{ matrix.log-level }}-database.tar.gz + tar -cvf - *.json | aws s3 cp - s3://${{ secrets.AWS_LOG_BUCKET_NAME }}/core-pipeline/${{ github.run_number }}/${{ github.run_attempt }}/htcmock-${{ matrix.target.queue }}-${{ matrix.target.object }}-${{ matrix.target.log-level }}-${{ matrix.target.cinit }}-database.tar.gz testWindowsDocker: needs: diff --git a/Adaptors/MongoDB/src/Common/IMongoDataModelMapping.cs b/Adaptors/MongoDB/src/Common/IMongoDataModelMapping.cs index 7a3399a48..74bf7f6e3 100644 --- a/Adaptors/MongoDB/src/Common/IMongoDataModelMapping.cs +++ b/Adaptors/MongoDB/src/Common/IMongoDataModelMapping.cs @@ -17,6 +17,8 @@ using System.Threading.Tasks; +using ArmoniK.Core.Common.Injection.Options.Database; + using MongoDB.Driver; namespace ArmoniK.Core.Adapters.MongoDB.Common; @@ -25,10 +27,44 @@ public interface IMongoDataModelMapping { string CollectionName { get; } + /// + /// Setup indexes for the collection + /// Can be called multiple times + /// + /// MongoDB Client session + /// MongoDDB Collection in which to insert data + /// Options for MongoDB + /// + /// Task representing the asynchronous execution of the method + /// Task InitializeIndexesAsync(IClientSessionHandle sessionHandle, IMongoCollection collection, Options.MongoDB options); + /// + /// Setup sharding for the collection + /// Can be called multiple times + /// + /// MongoDB Client session + /// Options for MongoDB + /// + /// Task representing the asynchronous execution of the method + /// Task ShardCollectionAsync(IClientSessionHandle sessionHandle, Options.MongoDB options); + + /// + /// Insert data into the collection after its creation. + /// Can be called multiple times + /// + /// MongoDB Client session + /// MongoDDB Collection in which to insert data + /// Data to insert + /// + /// Task representing the asynchronous execution of the method + /// + Task InitializeCollectionAsync(IClientSessionHandle sessionHandle, + IMongoCollection collection, + InitDatabase initDatabase) + => Task.CompletedTask; } diff --git a/Adaptors/MongoDB/src/Common/MongoCollectionProvider.cs b/Adaptors/MongoDB/src/Common/MongoCollectionProvider.cs index 578bcdffa..c37cfdd59 100644 --- a/Adaptors/MongoDB/src/Common/MongoCollectionProvider.cs +++ b/Adaptors/MongoDB/src/Common/MongoCollectionProvider.cs @@ -16,12 +16,14 @@ // along with this program. If not, see . using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using ArmoniK.Core.Base; using ArmoniK.Core.Base.DataStructures; using ArmoniK.Core.Common; +using ArmoniK.Core.Common.Injection.Options.Database; using JetBrains.Annotations; @@ -40,6 +42,7 @@ public class MongoCollectionProvider : IInitializable, IAs private IMongoCollection? mongoCollection_; public MongoCollectionProvider(Options.MongoDB options, + InitDatabase initDatabase, SessionProvider sessionProvider, IMongoDatabase mongoDatabase, ILogger> logger, @@ -52,6 +55,7 @@ public MongoCollectionProvider(Options.MongoDB options, } Initialization = InitializeAsync(options, + initDatabase, sessionProvider, mongoDatabase, logger, @@ -88,6 +92,7 @@ public async Task Init(CancellationToken cancellationToken) } private static async Task> InitializeAsync(Options.MongoDB options, + InitDatabase initDatabase, SessionProvider sessionProvider, IMongoDatabase mongoDatabase, ILogger> logger, @@ -96,47 +101,55 @@ private static async Task> InitializeAsync(Options.Mongo var model = new TModelMapping(); Exception? lastException = null; - for (var collectionRetry = 1; collectionRetry < options.MaxRetries; collectionRetry++) + if (initDatabase.Init) { - lastException = null; - try - { - await mongoDatabase.CreateCollectionAsync(model.CollectionName, - null, - cancellationToken) - .ConfigureAwait(false); - break; - } - catch (MongoCommandException ex) when (ex.CodeName == "NamespaceExists") + for (var collectionRetry = 1; collectionRetry < options.MaxRetries; collectionRetry++) { - logger.LogDebug(ex, - "Use already existing instance of Collection {CollectionName}", - model.CollectionName); - break; + lastException = null; + try + { + await mongoDatabase.CreateCollectionAsync(model.CollectionName, + null, + cancellationToken) + .ConfigureAwait(false); + break; + } + catch (MongoCommandException ex) when (ex.CodeName == "NamespaceExists") + { + logger.LogDebug(ex, + "Use already existing instance of Collection {CollectionName}", + model.CollectionName); + break; + } + catch (Exception ex) + { + lastException = ex; + logger.LogDebug(ex, + "Retrying to create Collection {CollectionName}", + model.CollectionName); + await Task.Delay(1000 * collectionRetry, + cancellationToken) + .ConfigureAwait(false); + } } - catch (Exception ex) + + if (lastException is not null) { - lastException = ex; - logger.LogDebug(ex, - "Retrying to create Collection {CollectionName}", - model.CollectionName); - await Task.Delay(1000 * collectionRetry, - cancellationToken) - .ConfigureAwait(false); + throw new TimeoutException($"Create {model.CollectionName}: Max retries reached", + lastException); } } - if (lastException is not null) - { - throw new TimeoutException($"Create {model.CollectionName}: Max retries reached", - lastException); - } - var output = mongoDatabase.GetCollection(model.CollectionName); await sessionProvider.Init(cancellationToken) .ConfigureAwait(false); var session = sessionProvider.Get(); + if (!initDatabase.Init) + { + return output; + } + for (var indexRetry = 1; indexRetry < options.MaxRetries; indexRetry++) { lastException = null; @@ -192,9 +205,39 @@ await Task.Delay(1000 * indexRetry, } } + for (var indexRetry = 1; indexRetry < options.MaxRetries; indexRetry++) + { + lastException = null; + try + { + await model.InitializeCollectionAsync(session, + output, + initDatabase) + .ConfigureAwait(false); + break; + } + catch (MongoBulkWriteException e) when (e.WriteErrors.All(error => error.Category == ServerErrorCategory.DuplicateKey)) + { + logger.LogDebug(e, + "Values were already present within the collection {CollectionName}", + model.CollectionName); + break; + } + catch (Exception ex) + { + lastException = ex; + logger.LogDebug(ex, + "Retrying to initialize {CollectionName} collection", + model.CollectionName); + await Task.Delay(1000 * indexRetry, + cancellationToken) + .ConfigureAwait(false); + } + } + if (lastException is not null) { - throw new TimeoutException($"Init Index or shard for {model.CollectionName}: Max retries reached", + throw new TimeoutException($"Init for {model.CollectionName}: Max retries reached", lastException); } diff --git a/Adaptors/MongoDB/src/Table/DataModel/Auth/AuthDataModelMapping.cs b/Adaptors/MongoDB/src/Table/DataModel/Auth/AuthDataModelMapping.cs index bae6c44a5..856774458 100644 --- a/Adaptors/MongoDB/src/Table/DataModel/Auth/AuthDataModelMapping.cs +++ b/Adaptors/MongoDB/src/Table/DataModel/Auth/AuthDataModelMapping.cs @@ -15,10 +15,12 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +using System.Linq; using System.Threading.Tasks; using ArmoniK.Core.Adapters.MongoDB.Common; using ArmoniK.Core.Common.Auth.Authentication; +using ArmoniK.Core.Common.Injection.Options.Database; using MongoDB.Bson; using MongoDB.Bson.Serialization; @@ -78,7 +80,21 @@ await collection.Indexes.CreateManyAsync(sessionHandle, .ConfigureAwait(false); } + /// public Task ShardCollectionAsync(IClientSessionHandle sessionHandle, Options.MongoDB options) => Task.CompletedTask; + + /// + public async Task InitializeCollectionAsync(IClientSessionHandle sessionHandle, + IMongoCollection collection, + InitDatabase initDatabase) + { + if (initDatabase.Auths.Any()) + { + await collection.InsertManyAsync(sessionHandle, + initDatabase.Auths) + .ConfigureAwait(false); + } + } } diff --git a/Adaptors/MongoDB/src/Table/DataModel/Auth/RoleDataModelMapping.cs b/Adaptors/MongoDB/src/Table/DataModel/Auth/RoleDataModelMapping.cs index 5da24a1cc..927715ff9 100644 --- a/Adaptors/MongoDB/src/Table/DataModel/Auth/RoleDataModelMapping.cs +++ b/Adaptors/MongoDB/src/Table/DataModel/Auth/RoleDataModelMapping.cs @@ -16,10 +16,12 @@ // along with this program. If not, see . using System; +using System.Linq; using System.Threading.Tasks; using ArmoniK.Core.Adapters.MongoDB.Common; using ArmoniK.Core.Common.Auth.Authentication; +using ArmoniK.Core.Common.Injection.Options.Database; using MongoDB.Bson.Serialization; using MongoDB.Driver; @@ -74,7 +76,21 @@ await collection.Indexes.CreateManyAsync(sessionHandle, .ConfigureAwait(false); } + /// public Task ShardCollectionAsync(IClientSessionHandle sessionHandle, Options.MongoDB options) => Task.CompletedTask; + + /// + public async Task InitializeCollectionAsync(IClientSessionHandle sessionHandle, + IMongoCollection collection, + InitDatabase initDatabase) + { + if (initDatabase.Roles.Any()) + { + await collection.InsertManyAsync(sessionHandle, + initDatabase.Roles) + .ConfigureAwait(false); + } + } } diff --git a/Adaptors/MongoDB/src/Table/DataModel/Auth/UserDataModelMapping.cs b/Adaptors/MongoDB/src/Table/DataModel/Auth/UserDataModelMapping.cs index 56d22fcbd..048647c8a 100644 --- a/Adaptors/MongoDB/src/Table/DataModel/Auth/UserDataModelMapping.cs +++ b/Adaptors/MongoDB/src/Table/DataModel/Auth/UserDataModelMapping.cs @@ -16,10 +16,12 @@ // along with this program. If not, see . using System; +using System.Linq; using System.Threading.Tasks; using ArmoniK.Core.Adapters.MongoDB.Common; using ArmoniK.Core.Common.Auth.Authentication; +using ArmoniK.Core.Common.Injection.Options.Database; using MongoDB.Bson.Serialization; using MongoDB.Driver; @@ -74,7 +76,21 @@ await collection.Indexes.CreateManyAsync(sessionHandle, .ConfigureAwait(false); } + /// public Task ShardCollectionAsync(IClientSessionHandle sessionHandle, Options.MongoDB options) => Task.CompletedTask; + + /// + public async Task InitializeCollectionAsync(IClientSessionHandle sessionHandle, + IMongoCollection collection, + InitDatabase initDatabase) + { + if (initDatabase.Users.Any()) + { + await collection.InsertManyAsync(sessionHandle, + initDatabase.Users) + .ConfigureAwait(false); + } + } } diff --git a/Adaptors/MongoDB/src/Table/DataModel/PartitionDataModelMapping.cs b/Adaptors/MongoDB/src/Table/DataModel/PartitionDataModelMapping.cs index 2c22bc412..8cb8b4fa3 100644 --- a/Adaptors/MongoDB/src/Table/DataModel/PartitionDataModelMapping.cs +++ b/Adaptors/MongoDB/src/Table/DataModel/PartitionDataModelMapping.cs @@ -15,9 +15,11 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +using System.Linq; using System.Threading.Tasks; using ArmoniK.Core.Adapters.MongoDB.Common; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Storage; using MongoDB.Bson.Serialization; @@ -91,7 +93,21 @@ await collection.Indexes.CreateManyAsync(sessionHandle, .ConfigureAwait(false); } + /// public Task ShardCollectionAsync(IClientSessionHandle sessionHandle, Options.MongoDB options) => Task.CompletedTask; + + /// + public async Task InitializeCollectionAsync(IClientSessionHandle sessionHandle, + IMongoCollection collection, + InitDatabase initDatabase) + { + if (initDatabase.Partitions.Any()) + { + await collection.InsertManyAsync(sessionHandle, + initDatabase.Partitions) + .ConfigureAwait(false); + } + } } diff --git a/Adaptors/MongoDB/tests/IndexTest.cs b/Adaptors/MongoDB/tests/IndexTest.cs index 9a44df275..24b6c127d 100644 --- a/Adaptors/MongoDB/tests/IndexTest.cs +++ b/Adaptors/MongoDB/tests/IndexTest.cs @@ -21,7 +21,10 @@ using ArmoniK.Core.Adapters.MongoDB.Table.DataModel; using ArmoniK.Core.Common.Auth.Authentication; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Storage; +using ArmoniK.Core.Utils; using EphemeralMongo; @@ -77,6 +80,9 @@ public void StartUp() logger); services.AddSingleton(ActivitySource); services.AddTransient(_ => client_); + services.AddInitializedOption(configuration, + InitServices.SettingSection); + services.AddSingleton(); services.AddLogging(); provider_ = services.BuildServiceProvider(new ServiceProviderOptions diff --git a/Adaptors/MongoDB/tests/InjectionTests.cs b/Adaptors/MongoDB/tests/InjectionTests.cs index d46387388..ab8fc22b5 100644 --- a/Adaptors/MongoDB/tests/InjectionTests.cs +++ b/Adaptors/MongoDB/tests/InjectionTests.cs @@ -20,6 +20,8 @@ using System.Diagnostics; using ArmoniK.Core.Adapters.MongoDB.Options; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Storage; using ArmoniK.Core.Utils; @@ -100,6 +102,9 @@ public void SetUp() var services = new ServiceCollection(); services.AddMongoComponents(configuration_, logger); + services.AddInitializedOption(configuration_, + InitServices.SettingSection); + services.AddSingleton(); services.AddSingleton(ActivitySource); services.AddLogging(); provider_ = services.BuildServiceProvider(new ServiceProviderOptions diff --git a/Adaptors/MongoDB/tests/MongoDatabaseProvider.cs b/Adaptors/MongoDB/tests/MongoDatabaseProvider.cs index 3d6232496..2ae12b790 100644 --- a/Adaptors/MongoDB/tests/MongoDatabaseProvider.cs +++ b/Adaptors/MongoDB/tests/MongoDatabaseProvider.cs @@ -20,6 +20,8 @@ using System.Diagnostics; using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; +using ArmoniK.Core.Utils; using EphemeralMongo; @@ -103,7 +105,8 @@ public MongoDatabaseProvider(bool useSingleNodeReplicaSet }; var configuration = new ConfigurationManager(); - configuration.AddInMemoryCollection(minimalConfig); + configuration.AddInMemoryCollection(minimalConfig) + .AddEnvironmentVariables(); var serviceCollection = new ServiceCollection(); serviceCollection.AddMongoStorages(configuration, @@ -111,6 +114,9 @@ public MongoDatabaseProvider(bool useSingleNodeReplicaSet serviceCollection.AddClientSubmitterAuthenticationStorage(configuration); serviceCollection.AddSingleton(ActivitySource); serviceCollection.AddTransient(_ => client); + serviceCollection.AddInitializedOption(configuration, + InitServices.SettingSection); + serviceCollection.AddSingleton(); serviceCollection.AddLogging(builder => builder.AddSerilog(loggerSerilog)); serviceConfigurator?.Invoke(serviceCollection); diff --git a/Adaptors/MongoDB/tests/SessionProviderTests.cs b/Adaptors/MongoDB/tests/SessionProviderTests.cs index e4e7b7743..2889f11a7 100644 --- a/Adaptors/MongoDB/tests/SessionProviderTests.cs +++ b/Adaptors/MongoDB/tests/SessionProviderTests.cs @@ -23,6 +23,9 @@ using ArmoniK.Core.Adapters.MongoDB.Common; using ArmoniK.Core.Base.DataStructures; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; +using ArmoniK.Core.Utils; using EphemeralMongo; @@ -88,6 +91,9 @@ public void SetUp() logger); services.AddSingleton(ActivitySource); services.AddTransient(_ => client_); + services.AddInitializedOption(configuration, + InitServices.SettingSection); + services.AddSingleton(); services.AddLogging(); provider_ = services.BuildServiceProvider(new ServiceProviderOptions diff --git a/Common/src/Injection/Options/Database/Authentication.cs b/Common/src/Injection/Options/Database/Authentication.cs new file mode 100644 index 000000000..07d8a254a --- /dev/null +++ b/Common/src/Injection/Options/Database/Authentication.cs @@ -0,0 +1,46 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Collections.Generic; + +namespace ArmoniK.Core.Common.Injection.Options.Database; + +/// +/// Options fill authentication related data +/// +public record Authentication +{ + /// + /// Path to the section containing the values in the configuration object + /// + public const string SettingSection = nameof(Authentication); + + /// + /// User certificates used for authentication in a JSON string + /// + public List UserCertificates { get; init; } = new(); + + /// + /// Roles used for authentication in a JSON string + /// + public List Roles { get; init; } = new(); + + /// + /// Users used for authentication in a JSON string + /// + public List Users { get; init; } = new(); +} diff --git a/Common/src/Injection/Options/Database/Certificate.cs b/Common/src/Injection/Options/Database/Certificate.cs new file mode 100644 index 000000000..3f93279fe --- /dev/null +++ b/Common/src/Injection/Options/Database/Certificate.cs @@ -0,0 +1,61 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Text.Json; + +namespace ArmoniK.Core.Common.Injection.Options.Database; + +/// +/// Placeholder to deserialize certificates for Authentication provided by users +/// +public record Certificate +{ + /// + /// User associated to certificate + /// + public required string User { get; init; } + + // ReSharper disable once InconsistentNaming + /// + /// Certificate common name + /// + public required string CN { get; init; } + + /// + /// Certificate fingerprint + /// + public string? Fingerprint { get; init; } + + /// + /// Convert to JSON + /// + /// + /// representing the JSON object of this instance + /// + public string ToJson() + => JsonSerializer.Serialize(this); + + /// + /// Build a from a JSON + /// + /// JSON value + /// + /// built from the provided JSON + /// + public static Certificate FromJson(string json) + => JsonSerializer.Deserialize(json)!; +} diff --git a/Common/src/Injection/Options/Database/InitDatabase.cs b/Common/src/Injection/Options/Database/InitDatabase.cs new file mode 100644 index 000000000..df814902f --- /dev/null +++ b/Common/src/Injection/Options/Database/InitDatabase.cs @@ -0,0 +1,105 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Collections.Generic; +using System.Linq; + +using ArmoniK.Core.Common.Auth.Authentication; +using ArmoniK.Core.Common.Storage; +using ArmoniK.Utils; + +namespace ArmoniK.Core.Common.Injection.Options.Database; + +/// +/// Convert into objects we can insert into the database +/// Also used to init database collections +/// +public class InitDatabase +{ + /// + /// Collection of data to insert in the database for Authentication during ArmoniK initialization + /// + public readonly ICollection Auths; + + /// + /// Whether to init the database + /// + public readonly bool Init; + + /// + /// Collection of data to insert in the database for Partitions during ArmoniK initialization + /// + public readonly ICollection Partitions; + + /// + /// Collection of data to insert in the database for Roles during ArmoniK initialization + /// + public readonly ICollection Roles; + + /// + /// Collection of data to insert in the database for Users during ArmoniK initialization + /// + public readonly ICollection Users; + + + /// + /// Instantiate from the configurations received from the Dependency Injection + /// + /// Data structure containing the raw data + public InitDatabase(InitServices initServices) + { + Init = initServices.InitDatabase; + + Roles = initServices.Authentication.Roles.Select(Role.FromJson) + .OrderBy(role => role.Name) + .Select((role, + i) => new RoleData(i.ToString(), + role.Name, + role.Permissions.ToArray())) + .AsICollection(); + + Users = initServices.Authentication.Users.Select(User.FromJson) + .OrderBy(user => user.Name) + .Select((user, + i) => new UserData(i.ToString(), + user.Name, + user.Roles.ToArray())) + .AsICollection(); + + var userDic = Users.ToDictionary(data => data.Username, + data => data.UserId); + + Auths = initServices.Authentication.UserCertificates.Select(Certificate.FromJson) + .OrderBy(certificate => (certificate.Fingerprint, certificate.CN)) + .Select((certificate, + i) => new AuthData(i.ToString(), + userDic[certificate.User], + certificate.CN, + certificate.Fingerprint)) + .AsICollection(); + + Partitions = initServices.Partitioning.Partitions.Select(Partition.FromJson) + .Select(partition => new PartitionData(partition.PartitionId, + partition.ParentPartitionIds, + partition.PodReserved, + partition.PodMax, + partition.PreemptionPercentage, + partition.Priority, + new PodConfiguration(partition.PodConfiguration))) + .AsICollection(); + } +} diff --git a/Common/src/Injection/Options/Database/Partition.cs b/Common/src/Injection/Options/Database/Partition.cs new file mode 100644 index 000000000..0b8e5cd35 --- /dev/null +++ b/Common/src/Injection/Options/Database/Partition.cs @@ -0,0 +1,86 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +using ArmoniK.Core.Common.Storage; + +namespace ArmoniK.Core.Common.Injection.Options.Database; + +/// +public record Partition +{ + /// + public required string PartitionId { get; init; } + + /// + public required IList ParentPartitionIds { get; init; } + + /// + public required int PodReserved { get; init; } + + /// + public required int PodMax { get; init; } + + /// + public required int PreemptionPercentage { get; init; } + + /// + public required int Priority { get; init; } + + /// + public IDictionary PodConfiguration { get; init; } = new Dictionary(); + + /// + public virtual bool Equals(Partition? other) + => !ReferenceEquals(other, + null) && PartitionId.Equals(other.PartitionId) && ParentPartitionIds.SequenceEqual(other.ParentPartitionIds) && + PodReserved.Equals(other.PodReserved) && PodMax.Equals(other.PodMax) && PreemptionPercentage.Equals(other.PreemptionPercentage) && + Priority.Equals(other.Priority) && PodConfiguration.SequenceEqual(other.PodConfiguration); + + /// + /// Convert to JSON + /// + /// + /// representing the JSON object of this instance + /// + public string ToJson() + => JsonSerializer.Serialize(this); + + /// + /// Build a from a JSON + /// + /// JSON value + /// + /// built from the provided JSON + /// + public static Partition FromJson(string json) + => JsonSerializer.Deserialize(json)!; + + /// + public override int GetHashCode() + => HashCode.Combine(PartitionId, + ParentPartitionIds, + PodReserved, + PodMax, + PreemptionPercentage, + Priority, + PodConfiguration); +} diff --git a/Common/src/Injection/Options/Database/Partitioning.cs b/Common/src/Injection/Options/Database/Partitioning.cs new file mode 100644 index 000000000..f21bd6cd4 --- /dev/null +++ b/Common/src/Injection/Options/Database/Partitioning.cs @@ -0,0 +1,36 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System.Collections.Generic; + +namespace ArmoniK.Core.Common.Injection.Options.Database; + +/// +/// Options to fill partitions related data +/// +public class Partitioning +{ + /// + /// Path to the section containing the values in the configuration object + /// + public const string SettingSection = nameof(Partitioning); + + /// + /// Partitions in a JSON string to insert into the database + /// + public List Partitions { get; init; } = new(); +} diff --git a/Common/src/Injection/Options/Database/Role.cs b/Common/src/Injection/Options/Database/Role.cs new file mode 100644 index 000000000..c2c53eb4e --- /dev/null +++ b/Common/src/Injection/Options/Database/Role.cs @@ -0,0 +1,68 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace ArmoniK.Core.Common.Injection.Options.Database; + +/// +/// Associate a Role to the permission it has +/// +public record Role +{ + /// + /// Role Name + /// + public required string Name { get; init; } + + /// + /// Permissions associated to the Role + /// + public required List Permissions { get; init; } + + /// + public virtual bool Equals(Role? other) + => !ReferenceEquals(null, + other) && Name.Equals(other.Name) && Permissions.SequenceEqual(other.Permissions); + + /// + /// Convert to JSON + /// + /// + /// representing the JSON object of this instance + /// + public string ToJson() + => JsonSerializer.Serialize(this); + + /// + /// Build a from a JSON + /// + /// JSON value + /// + /// built from the provided JSON + /// + public static Role FromJson(string json) + => JsonSerializer.Deserialize(json)!; + + /// + public override int GetHashCode() + => HashCode.Combine(Name, + Permissions); +} diff --git a/Common/src/Injection/Options/Database/User.cs b/Common/src/Injection/Options/Database/User.cs new file mode 100644 index 000000000..fed8c212e --- /dev/null +++ b/Common/src/Injection/Options/Database/User.cs @@ -0,0 +1,68 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace ArmoniK.Core.Common.Injection.Options.Database; + +/// +/// Associate a User to its Roles +/// +public record User +{ + /// + /// User Name + /// + public required string Name { get; init; } + + /// + /// Roles associated to the user + /// + public required List Roles { get; init; } + + /// + public virtual bool Equals(User? other) + => !ReferenceEquals(null, + other) && Name.Equals(other.Name) && Roles.SequenceEqual(other.Roles); + + /// + /// Convert to JSON + /// + /// + /// representing the JSON object of this instance + /// + public string ToJson() + => JsonSerializer.Serialize(this); + + /// + /// Build a from a JSON + /// + /// JSON value + /// + /// built from the provided JSON + /// + public static User FromJson(string json) + => JsonSerializer.Deserialize(json)!; + + /// + public override int GetHashCode() + => HashCode.Combine(Name, + Roles); +} diff --git a/Common/src/Injection/Options/InitServices.cs b/Common/src/Injection/Options/InitServices.cs new file mode 100644 index 000000000..52c8f9acf --- /dev/null +++ b/Common/src/Injection/Options/InitServices.cs @@ -0,0 +1,62 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using ArmoniK.Core.Common.Injection.Options.Database; + +namespace ArmoniK.Core.Common.Injection.Options; + +/// +/// Configuration for ArmoniK services +/// +public class InitServices +{ + /// + /// Path to the section containing the values in the configuration object + /// + public const string SettingSection = nameof(InitServices); + + /// + /// Authentication configurations + /// + public Authentication Authentication { get; set; } = new(); + + /// + /// Partitioning configurations + /// + public Partitioning Partitioning { get; set; } = new(); + + /// + /// Whether to perform database initialization (collection creation, indexing, sharding, data insertion, etc...). + /// + public bool InitDatabase { get; set; } = true; + + + /// + /// Whether to perform object storage initialization + /// + public bool InitObjectStorage { get; set; } = true; + + /// + /// Whether to perform queue initialization + /// + public bool InitQueue { get; set; } = true; + + /// + /// Stop the service after performing initialization + /// + public bool StopAfterInit { get; set; } = false; +} diff --git a/Common/src/Storage/PartitionData.cs b/Common/src/Storage/PartitionData.cs index 7053bdc6f..d8497ab6d 100644 --- a/Common/src/Storage/PartitionData.cs +++ b/Common/src/Storage/PartitionData.cs @@ -28,7 +28,7 @@ namespace ArmoniK.Core.Common.Storage; /// Max number of pods /// Percentage of pods that can be preempted /// Priority of the partition -/// Pod configuration used to select machines +/// Configuration for compute plane instances in this Partition public record PartitionData(string PartitionId, IList ParentPartitionIds, int PodReserved, diff --git a/Common/tests/Helpers/TestDatabaseProvider.cs b/Common/tests/Helpers/TestDatabaseProvider.cs index 388fe18c0..2b0aeaab9 100644 --- a/Common/tests/Helpers/TestDatabaseProvider.cs +++ b/Common/tests/Helpers/TestDatabaseProvider.cs @@ -25,7 +25,10 @@ using ArmoniK.Core.Adapters.MongoDB.Common; using ArmoniK.Core.Common.Auth.Authentication; using ArmoniK.Core.Common.Injection; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Storage; +using ArmoniK.Core.Utils; using EphemeralMongo; @@ -138,6 +141,9 @@ public TestDatabaseProvider(Action? collectionConfigurato .AddClientSubmitterAuthenticationStorage(builder.Configuration) .AddClientSubmitterAuthServices(builder.Configuration, out _) + .AddInitializedOption(builder.Configuration, + InitServices.SettingSection) + .AddSingleton() .Configure(o => o.CopyFrom(AuthenticatorOptions.DefaultNoAuth)) .AddLogging() .AddSingleton(loggerProvider.CreateLogger("root")) diff --git a/Common/tests/Helpers/TestPollsterProvider.cs b/Common/tests/Helpers/TestPollsterProvider.cs index 41ad30a17..b08cc6cf1 100644 --- a/Common/tests/Helpers/TestPollsterProvider.cs +++ b/Common/tests/Helpers/TestPollsterProvider.cs @@ -28,6 +28,8 @@ using ArmoniK.Core.Adapters.MongoDB; using ArmoniK.Core.Base; using ArmoniK.Core.Common.gRPC.Services; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Meter; using ArmoniK.Core.Common.Pollster; using ArmoniK.Core.Common.Pollster.TaskProcessingChecker; @@ -168,6 +170,9 @@ public TestPollsterProvider(IWorkerStreamHandler workerStreamHandler, .AddSingleton() .AddOption(builder.Configuration, Injection.Options.Pollster.SettingSection) + .AddInitializedOption(builder.Configuration, + InitServices.SettingSection) + .AddSingleton() .AddSingleton(sp => new ExceptionManager.Options(sp.GetRequiredService() .GraceDelay, sp.GetRequiredService() diff --git a/Common/tests/Helpers/TestTaskHandlerProvider.cs b/Common/tests/Helpers/TestTaskHandlerProvider.cs index ea4dc6064..2786d0e9d 100644 --- a/Common/tests/Helpers/TestTaskHandlerProvider.cs +++ b/Common/tests/Helpers/TestTaskHandlerProvider.cs @@ -25,6 +25,8 @@ using ArmoniK.Core.Adapters.MongoDB; using ArmoniK.Core.Base; using ArmoniK.Core.Common.gRPC.Services; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Meter; using ArmoniK.Core.Common.Pollster; using ArmoniK.Core.Common.Pollster.TaskProcessingChecker; @@ -158,6 +160,9 @@ public TestTaskHandlerProvider(IWorkerStreamHandler workerStreamHandler, Injection.Options.Submitter.SettingSection) .AddOption(builder.Configuration, Injection.Options.Pollster.SettingSection) + .AddInitializedOption(builder.Configuration, + InitServices.SettingSection) + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Common/tests/StaticInitTests.cs b/Common/tests/StaticInitTests.cs new file mode 100644 index 000000000..a5ac69827 --- /dev/null +++ b/Common/tests/StaticInitTests.cs @@ -0,0 +1,316 @@ +// This file is part of the ArmoniK project +// +// Copyright (C) ANEO, 2021-2024. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY, without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +using System; +using System.Collections.Generic; + +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; +using ArmoniK.Core.Common.Utils; +using ArmoniK.Core.Utils; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using NUnit.Framework; + +namespace ArmoniK.Core.Common.Tests; + +[TestFixture] +internal class StaticInitTests +{ + [SetUp] + public void SetUp() + { + Dictionary baseConfig = new() + { + { + $"{CertificatePath}:0", Cert1.ToJson() + }, + { + $"{CertificatePath}:1", Cert2.ToJson() + }, + { + $"{UserPath}:0", User1.ToJson() + }, + { + $"{UserPath}:1", User2.ToJson() + }, + { + $"{RolePath}:0", Role1.ToJson() + }, + { + $"{RolePath}:1", Role2.ToJson() + }, + { + $"{PartitionPath}:0", PartitionData1.ToJson() + }, + { + $"{PartitionPath}:1", PartitionData2.ToJson() + }, + }; + + Environment.SetEnvironmentVariable(CertificatePath.Replace(":", + "__") + "__2", + Cert3.ToJson()); + Environment.SetEnvironmentVariable(UserPath.Replace(":", + "__") + "__2", + User3.ToJson()); + Environment.SetEnvironmentVariable(RolePath.Replace(":", + "__") + "__2", + Role3.ToJson()); + Environment.SetEnvironmentVariable(PartitionPath.Replace(":", + "__") + "__2", + PartitionData3.ToJson()); + + configuration_ = new ConfigurationManager(); + configuration_.AddInMemoryCollection(baseConfig); + configuration_.AddEnvironmentVariables(); + + var services = new ServiceCollection(); + services.AddOption(configuration_, + InitServices.SettingSection); + services.AddSingleton(); + provider_ = services.BuildServiceProvider(); + + logger_ = new LoggerInit(configuration_).GetLogger(); + } + + private static readonly Certificate Cert1 = new() + { + Fingerprint = "Fingerprint1", + CN = "CN1", + User = "User1", + }; + + private static readonly Certificate Cert2 = new() + { + Fingerprint = "Fingerprint2", + CN = "CN2", + User = "User2", + }; + + private static readonly Certificate Cert3 = new() + { + Fingerprint = "Fingerprint3", + CN = "CN3", + User = "User3", + }; + + private static readonly User User1 = new() + { + Name = "User1", + Roles = new List + { + "Role1", + }, + }; + + private static readonly User User2 = new() + { + Name = "User2", + Roles = new List + { + "Role1", + "Role2", + }, + }; + + private static readonly User User3 = new() + { + Name = "User3", + Roles = new List + { + "Role3", + }, + }; + + private static readonly Role Role1 = new() + { + Name = "Role1", + Permissions = new List + { + "Perm1", + }, + }; + + private static readonly Role Role2 = new() + { + Name = "Role2", + Permissions = new List + { + "Perm1", + "Perm2", + }, + }; + + private static readonly Role Role3 = new() + { + Name = "Role3", + Permissions = new List + { + "Perm3", + }, + }; + + private static readonly Partition PartitionData1 = new() + { + Priority = 2, + PodMax = 2, + PodReserved = 2, + PreemptionPercentage = 2, + PartitionId = "Partition1", + ParentPartitionIds = new List + { + "PartitionParent1", + }, + PodConfiguration = new Dictionary + { + { + "key1", "val1" + }, + { + "key2", "val2" + }, + }, + }; + + private static readonly Partition PartitionData2 = new() + { + Priority = 2, + PodMax = 2, + PodReserved = 2, + PreemptionPercentage = 2, + PartitionId = "Partition2", + ParentPartitionIds = new List + { + "PartitionParent1", + "PartitionParent2", + }, + PodConfiguration = new Dictionary + { + { + "key1", "val1" + }, + { + "key3", "val3" + }, + }, + }; + + private static readonly Partition PartitionData3 = new() + { + Priority = 2, + PodMax = 2, + PodReserved = 2, + PreemptionPercentage = 2, + PartitionId = "Partition3", + ParentPartitionIds = new List + { + "PartitionParent1", + "PartitionParent3", + }, + }; + + private const string CertificatePath = $"{InitServices.SettingSection}:{Authentication.SettingSection}:{nameof(Authentication.UserCertificates)}"; + private const string UserPath = $"{InitServices.SettingSection}:{Authentication.SettingSection}:{nameof(Authentication.Users)}"; + private const string RolePath = $"{InitServices.SettingSection}:{Authentication.SettingSection}:{nameof(Authentication.Roles)}"; + private const string PartitionPath = $"{InitServices.SettingSection}:{Partitioning.SettingSection}:{nameof(Partitioning.Partitions)}"; + + private ServiceProvider? provider_; + private ConfigurationManager? configuration_; + private ILogger? logger_; + + [Test] + public void InitServicesNotNull() + { + var init = provider_!.GetRequiredService(); + Assert.NotNull(init); + Assert.True(init.InitDatabase); + Assert.True(init.InitObjectStorage); + Assert.True(init.InitQueue); + } + + [Test] + public void CountShouldBePositive() + { + var init = provider_!.GetRequiredService(); + logger_!.LogInformation("{@init}", + init); + Assert.AreEqual(3, + init.Authentication.UserCertificates.Count); + Assert.AreEqual(3, + init.Authentication.Users.Count); + Assert.AreEqual(3, + init.Authentication.Roles.Count); + Assert.AreEqual(3, + init.Partitioning.Partitions.Count); + + Assert.AreEqual(Cert1, + Certificate.FromJson(init.Authentication.UserCertificates[0])); + Assert.AreEqual(Cert2, + Certificate.FromJson(init.Authentication.UserCertificates[1])); + Assert.AreEqual(Cert3, + Certificate.FromJson(init.Authentication.UserCertificates[2])); + + Assert.AreEqual(User1, + User.FromJson(init.Authentication.Users[0])); + Assert.AreEqual(User2, + User.FromJson(init.Authentication.Users[1])); + Assert.AreEqual(User3, + User.FromJson(init.Authentication.Users[2])); + + Assert.AreEqual(Role1, + Role.FromJson(init.Authentication.Roles[0])); + Assert.AreEqual(Role2, + Role.FromJson(init.Authentication.Roles[1])); + Assert.AreEqual(Role3, + Role.FromJson(init.Authentication.Roles[2])); + + Assert.AreEqual(PartitionData1, + Partition.FromJson(init.Partitioning.Partitions[0])); + Assert.AreEqual(PartitionData2, + Partition.FromJson(init.Partitioning.Partitions[1])); + Assert.AreEqual(PartitionData3, + Partition.FromJson(init.Partitioning.Partitions[2])); + + var initDb = provider_!.GetRequiredService(); + Assert.AreEqual(3, + initDb.Users.Count); + Assert.AreEqual(3, + initDb.Auths.Count); + Assert.AreEqual(3, + initDb.Roles.Count); + Assert.AreEqual(3, + initDb.Partitions.Count); + } + + [Test] + public void NoFingerprintShouldSucceed() + { + var cert = Certificate.FromJson("{\"User\": \"User1\", \"CN\": \"CN1\"}"); + Assert.IsNull(cert.Fingerprint); + } + + [Test] + public void NullFingerprintShouldSucceed() + { + var cert = Certificate.FromJson("{\"User\": \"User1\", \"CN\": \"CN1\", \"Fingerprint\": null}"); + Assert.IsNull(cert.Fingerprint); + } +} diff --git a/Common/tests/Submitter/SubmitterTests.cs b/Common/tests/Submitter/SubmitterTests.cs index fe9f173de..826741392 100644 --- a/Common/tests/Submitter/SubmitterTests.cs +++ b/Common/tests/Submitter/SubmitterTests.cs @@ -32,6 +32,8 @@ using ArmoniK.Core.Common.gRPC.Convertors; using ArmoniK.Core.Common.gRPC.Services; using ArmoniK.Core.Common.gRPC.Validators; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Storage; using ArmoniK.Core.Common.Tests.Helpers; using ArmoniK.Core.Utils; @@ -128,6 +130,9 @@ public async Task SetUp() .AddSingleton() .AddOption(configuration, Injection.Options.Submitter.SettingSection) + .AddInitializedOption(configuration, + InitServices.SettingSection) + .AddSingleton() .AddSingleton(pushQueueStorage_); var provider = services.BuildServiceProvider(new ServiceProviderOptions diff --git a/Common/tests/TestBase/AuthenticationTableTestBase.cs b/Common/tests/TestBase/AuthenticationTableTestBase.cs index 04730f6de..7b76bd7b3 100644 --- a/Common/tests/TestBase/AuthenticationTableTestBase.cs +++ b/Common/tests/TestBase/AuthenticationTableTestBase.cs @@ -25,6 +25,8 @@ using ArmoniK.Core.Base.DataStructures; using ArmoniK.Core.Common.Auth.Authentication; using ArmoniK.Core.Common.Auth.Authorization.Permissions; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -43,6 +45,16 @@ private static bool CheckForSkipSetup() public async Task SetUp() { + Environment.SetEnvironmentVariable(CertificatePath.Replace(":", + "__") + "__0", + CertEnv.ToJson()); + Environment.SetEnvironmentVariable(UserPath.Replace(":", + "__") + "__0", + UserEnv.ToJson()); + Environment.SetEnvironmentVariable(RolePath.Replace(":", + "__") + "__0", + RoleEnv.ToJson()); + GetAuthSource(); if (!RunTests || CheckForSkipSetup()) @@ -61,6 +73,37 @@ public async Task SetUp() public virtual void TearDown() => RunTests = false; + private static readonly Certificate CertEnv = new() + { + Fingerprint = "FingerprintEnv", + CN = "CNEnv", + User = "UserEnv", + }; + + + private static readonly User UserEnv = new() + { + Name = "UserEnv", + Roles = new List + { + "RoleEnv", + }, + }; + + + private static readonly Role RoleEnv = new() + { + Name = "RoleEnv", + Permissions = new List + { + "PermEnv", + }, + }; + + private const string CertificatePath = $"{InitServices.SettingSection}:{Authentication.SettingSection}:{nameof(Authentication.UserCertificates)}"; + private const string UserPath = $"{InitServices.SettingSection}:{Authentication.SettingSection}:{nameof(Authentication.Users)}"; + private const string RolePath = $"{InitServices.SettingSection}:{Authentication.SettingSection}:{nameof(Authentication.Roles)}"; + static AuthenticationTableTestBase() { Roles = new List @@ -375,6 +418,9 @@ public void GetIdentityFromNameShouldFail(string name) [TestCase("User2", "Role1", true)] + [TestCase("UserEnv", + "RoleEnv", + true)] [TestCase("User2", "RoleDontExist", false)] diff --git a/Common/tests/TestBase/PartitionTableTestBase.cs b/Common/tests/TestBase/PartitionTableTestBase.cs index 99b05022f..fbea71f2f 100644 --- a/Common/tests/TestBase/PartitionTableTestBase.cs +++ b/Common/tests/TestBase/PartitionTableTestBase.cs @@ -15,6 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -24,6 +25,8 @@ using ArmoniK.Core.Base.DataStructures; using ArmoniK.Core.Common.Exceptions; using ArmoniK.Core.Common.gRPC; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Storage; using ArmoniK.Core.Common.Tests.ListPartitionsRequestExt; @@ -39,6 +42,10 @@ public class PartitionTableTestBase [SetUp] public async Task SetUp() { + Environment.SetEnvironmentVariable(PartitionPath.Replace(":", + "__") + "__0", + PartitionEnv.ToJson()); + GetPartitionTableInstance(); if (!RunTests || CheckForSkipSetup()) @@ -105,6 +112,31 @@ public virtual void GetPartitionTableInstance() { } + private const string PartitionPath = $"{InitServices.SettingSection}:{Partitioning.SettingSection}:{nameof(Partitioning.Partitions)}"; + + + private static readonly Partition PartitionEnv = new() + { + Priority = 2, + PodMax = 2, + PodReserved = 2, + PreemptionPercentage = 2, + PartitionId = "Partition1", + ParentPartitionIds = new List + { + "PartitionParent1", + }, + PodConfiguration = new Dictionary + { + { + "key1", "val1" + }, + { + "key2", "val2" + }, + }, + }; + [Test] [Category("SkipSetUp")] public async Task InitShouldSucceed() @@ -151,6 +183,24 @@ public async Task ReadPartitionAsyncShouldSucceed() } } + [Test] + public async Task ReadPartitionAsyncFromEnvShouldSucceed() + { + if (RunTests) + { + var result = await PartitionTable!.ReadPartitionAsync(PartitionEnv.PartitionId, + CancellationToken.None) + .ConfigureAwait(false); + + Assert.AreEqual(PartitionEnv.PartitionId, + result.PartitionId); + Assert.AreEqual(PartitionEnv.PodReserved, + result.PodReserved); + Assert.AreEqual(PartitionEnv.PodConfiguration, + result.PodConfiguration!.Configuration); + } + } + [Test] public Task ReadTaskAsyncShouldThrowException() { diff --git a/Compute/PollingAgent/src/Program.cs b/Compute/PollingAgent/src/Program.cs index 9d552849f..cad0f4989 100644 --- a/Compute/PollingAgent/src/Program.cs +++ b/Compute/PollingAgent/src/Program.cs @@ -30,6 +30,8 @@ using ArmoniK.Core.Base.DataStructures; using ArmoniK.Core.Common.gRPC.Services; using ArmoniK.Core.Common.Injection; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Meter; using ArmoniK.Core.Common.Pollster; using ArmoniK.Core.Common.Pollster.TaskProcessingChecker; @@ -108,6 +110,9 @@ public static async Task Main(string[] args) .AddSingleton() .AddInitializedOption(builder.Configuration, Submitter.SettingSection) + .AddInitializedOption(builder.Configuration, + InitServices.SettingSection) + .AddSingleton() .AddSingleton(pollsterOptions) .AddSingleton(new ExceptionManager.Options(pollsterOptions.GraceDelay, pollsterOptions.MaxErrorAllowed)) diff --git a/Control/Metrics/src/Program.cs b/Control/Metrics/src/Program.cs index 7d6e45635..2a2c0a15a 100644 --- a/Control/Metrics/src/Program.cs +++ b/Control/Metrics/src/Program.cs @@ -22,6 +22,8 @@ using ArmoniK.Core.Adapters.MongoDB; using ArmoniK.Core.Base.DataStructures; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Storage; using ArmoniK.Core.Common.Utils; using ArmoniK.Core.Control.Metrics.Options; @@ -64,6 +66,9 @@ public static async Task Main(string[] args) .AddMongoComponents(builder.Configuration, logger.GetLogger()) .AddSingleton(builder.Configuration.GetInitializedValue(MetricsExporter.SettingSection)) + .AddInitializedOption(builder.Configuration, + InitServices.SettingSection) + .AddSingleton() .AddHostedService() .AddControllers(); diff --git a/Control/PartitionMetrics/src/Program.cs b/Control/PartitionMetrics/src/Program.cs index 981cfea4f..69a9f7be7 100644 --- a/Control/PartitionMetrics/src/Program.cs +++ b/Control/PartitionMetrics/src/Program.cs @@ -22,6 +22,8 @@ using ArmoniK.Core.Adapters.MongoDB; using ArmoniK.Core.Base.DataStructures; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Storage; using ArmoniK.Core.Common.Utils; using ArmoniK.Core.Control.PartitionMetrics.Options; @@ -65,6 +67,9 @@ public static async Task Main(string[] args) logger.GetLogger()) .AddOption(builder.Configuration, MetricsExporter.SettingSection) + .AddInitializedOption(builder.Configuration, + InitServices.SettingSection) + .AddSingleton() .AddHostedService() .AddHttpClient() .AddControllers(); diff --git a/Control/Submitter/src/Program.cs b/Control/Submitter/src/Program.cs index 892906ca5..5af0b7e6f 100644 --- a/Control/Submitter/src/Program.cs +++ b/Control/Submitter/src/Program.cs @@ -32,6 +32,8 @@ using ArmoniK.Core.Common.gRPC; using ArmoniK.Core.Common.gRPC.Services; using ArmoniK.Core.Common.Injection; +using ArmoniK.Core.Common.Injection.Options; +using ArmoniK.Core.Common.Injection.Options.Database; using ArmoniK.Core.Common.Meter; using ArmoniK.Core.Common.Pollster; using ArmoniK.Core.Common.Storage; @@ -100,6 +102,9 @@ public static async Task Main(string[] args) .AddSingletonWithHealthCheck(nameof(ExceptionInterceptor)) .AddOption(builder.Configuration, Common.Injection.Options.Submitter.SettingSection) + .AddInitializedOption(builder.Configuration, + InitServices.SettingSection) + .AddSingleton() .AddSingleton(sp => new ExceptionManager.Options(TimeSpan.Zero, sp.GetRequiredService() .MaxErrorAllowed)) @@ -261,6 +266,12 @@ await authTable.Init(CancellationToken.None) await taskObjectFactory.ConfigureAwait(false); await taskPushQueueStorage.ConfigureAwait(false); + if (app.Services.GetRequiredService() + .StopAfterInit) + { + return 0; + } + await app.RunAsync() .ConfigureAwait(false); diff --git a/justfile b/justfile index 8b255cc74..bb351ff71 100644 --- a/justfile +++ b/justfile @@ -20,6 +20,7 @@ ingress := "true" prometheus := "true" grafana := "true" seq := "true" +cinit := "true" # Export them as terraform environment variables export TF_VAR_core_tag := tag @@ -30,6 +31,7 @@ export TF_VAR_num_partitions := partitions export TF_VAR_enable_grafana := grafana export TF_VAR_enable_seq := seq export TF_VAR_enable_prometheus := prometheus +export TF_VAR_container_init := cinit # Sets the queue diff --git a/terraform/locals.tf b/terraform/locals.tf index e5e81099e..e8fc1a95f 100644 --- a/terraform/locals.tf +++ b/terraform/locals.tf @@ -13,21 +13,22 @@ locals { "Serilog__MinimumLevel__Override__ArmoniK.Core.Common.Auth.Authentication.Authenticator" = "${var.serilog.loggin_level_routing}", "ASPNETCORE_ENVIRONMENT" = "${var.aspnet_core_env}" } - worker = merge(var.compute_plane.worker, { image = var.worker_image }) - queue = one(concat(module.queue_activemq, module.queue_rabbitmq, module.queue_artemis, module.queue_pubsub, module.queue_sqs, module.queue_none)) - database = module.database - object = one(concat(module.object_redis, module.object_minio, module.object_local)) + worker = merge(var.compute_plane.worker, { image = var.worker_image }) + queue = one(concat(module.queue_activemq, module.queue_rabbitmq, module.queue_artemis, module.queue_pubsub, module.queue_sqs, module.queue_none)) + database = module.database + object = one(concat(module.object_redis, module.object_minio, module.object_local)) + partition_env_vars = { for i in local.partitions : "InitServices__Partitioning__Partitions__${i}" => jsonencode(merge(var.partition_data, { PartitionId = "${var.partition_data.PartitionId}${i}" })) } env_maps = concat([ local.queue.generated_env_vars, local.object.generated_env_vars, local.database.generated_env_vars, local.logging_env_vars, - var.custom_env_vars + var.custom_env_vars, + local.partition_env_vars ], module.tracing[*].generated_env_vars) environment = merge(local.env_maps...) volumes = local.object.volumes submitter = merge(var.submitter, { tag = var.core_tag }) compute_plane = merge(var.compute_plane, { tag = var.core_tag }, { worker = local.worker }) - partition_list = { for i in local.partitions : i => merge(var.partition_data, { _id = "${var.partition_data._id}${i}" }) } polling_agent_names = toset([for v in module.compute_plane : v.polling_agent_name]) } diff --git a/terraform/main.tf b/terraform/main.tf index 56f9b4b2b..467aac05e 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -32,7 +32,6 @@ module "database" { image = var.database_image network = docker_network.armonik.id mongodb_params = var.mongodb_params - partition_list = local.partition_list } module "object_redis" { @@ -115,6 +114,7 @@ module "submitter" { generated_env_vars = local.environment log_driver = module.fluenbit.log_driver volumes = local.volumes + container_init = var.container_init } module "compute_plane" { @@ -129,6 +129,7 @@ module "compute_plane" { volumes = local.volumes network = docker_network.armonik.id log_driver = module.fluenbit.log_driver + container_init = var.container_init } module "metrics_exporter" { @@ -138,6 +139,7 @@ module "metrics_exporter" { network = docker_network.armonik.id generated_env_vars = local.environment log_driver = module.fluenbit.log_driver + container_init = var.container_init } module "partition_metrics_exporter" { @@ -148,6 +150,7 @@ module "partition_metrics_exporter" { generated_env_vars = local.environment metrics_env_vars = module.metrics_exporter.metrics_env_vars log_driver = module.fluenbit.log_driver + container_init = var.container_init } module "ingress" { diff --git a/terraform/modules/compute_plane/inputs.tf b/terraform/modules/compute_plane/inputs.tf index d599e9b0e..1320d30ed 100644 --- a/terraform/modules/compute_plane/inputs.tf +++ b/terraform/modules/compute_plane/inputs.tf @@ -50,3 +50,7 @@ variable "log_driver" { log_opts = map(string), }) } + +variable "container_init" { + type = bool +} diff --git a/terraform/modules/compute_plane/locals.tf b/terraform/modules/compute_plane/locals.tf index 4256b20e2..9458acfe3 100644 --- a/terraform/modules/compute_plane/locals.tf +++ b/terraform/modules/compute_plane/locals.tf @@ -8,6 +8,7 @@ locals { "InitWorker__WorkerCheckDelay=${var.polling_agent.worker_check_delay}", "Amqp__PartitionId=TestPartition${local.partition_chooser}", "PubSub__PartitionId=TestPartition${local.partition_chooser}", + "InitServices__InitDatabase=${!var.container_init}", ] common_env = [ "ComputePlane__WorkerChannel__SocketType=unixdomainsocket", diff --git a/terraform/modules/monitoring/metrics/inputs.tf b/terraform/modules/monitoring/metrics/inputs.tf index 3f7d806aa..84cbddb88 100644 --- a/terraform/modules/monitoring/metrics/inputs.tf +++ b/terraform/modules/monitoring/metrics/inputs.tf @@ -25,3 +25,7 @@ variable "log_driver" { log_opts = map(string), }) } + +variable "container_init" { + type = bool +} diff --git a/terraform/modules/monitoring/metrics/locals.tf b/terraform/modules/monitoring/metrics/locals.tf index bc0ffe04c..b5a00dd20 100644 --- a/terraform/modules/monitoring/metrics/locals.tf +++ b/terraform/modules/monitoring/metrics/locals.tf @@ -1,3 +1,6 @@ locals { + init_env = [ + "InitServices__InitDatabase=${!var.container_init}", + ] gen_env = [for k, v in var.generated_env_vars : "${k}=${v}"] -} \ No newline at end of file +} diff --git a/terraform/modules/monitoring/metrics/main.tf b/terraform/modules/monitoring/metrics/main.tf index 531674fbb..fa1670ec1 100644 --- a/terraform/modules/monitoring/metrics/main.tf +++ b/terraform/modules/monitoring/metrics/main.tf @@ -11,7 +11,7 @@ resource "docker_container" "metrics" { name = var.network } - env = local.gen_env + env = concat(local.gen_env, local.init_env) log_driver = var.log_driver.name log_opts = var.log_driver.log_opts diff --git a/terraform/modules/monitoring/partition_metrics/inputs.tf b/terraform/modules/monitoring/partition_metrics/inputs.tf index 3fbf4c6a3..2738e404d 100644 --- a/terraform/modules/monitoring/partition_metrics/inputs.tf +++ b/terraform/modules/monitoring/partition_metrics/inputs.tf @@ -29,3 +29,7 @@ variable "log_driver" { log_opts = map(string), }) } + +variable "container_init" { + type = bool +} diff --git a/terraform/modules/monitoring/partition_metrics/locals.tf b/terraform/modules/monitoring/partition_metrics/locals.tf index 4138bb4c7..468561575 100644 --- a/terraform/modules/monitoring/partition_metrics/locals.tf +++ b/terraform/modules/monitoring/partition_metrics/locals.tf @@ -1,4 +1,7 @@ locals { + init_env = [ + "InitServices__InitDatabase=${!var.container_init}", + ] env = [for k, v in var.metrics_env_vars : "${k}=${v}"] gen_env = [for k, v in var.generated_env_vars : "${k}=${v}"] -} \ No newline at end of file +} diff --git a/terraform/modules/monitoring/partition_metrics/main.tf b/terraform/modules/monitoring/partition_metrics/main.tf index 87d3fede5..b0f1230fb 100644 --- a/terraform/modules/monitoring/partition_metrics/main.tf +++ b/terraform/modules/monitoring/partition_metrics/main.tf @@ -11,7 +11,7 @@ resource "docker_container" "partition_metrics" { name = var.network } - env = concat(local.env, local.gen_env) + env = concat(local.env, local.gen_env, local.init_env) log_driver = var.log_driver.name log_opts = var.log_driver.log_opts diff --git a/terraform/modules/storage/database/mongo/inputs.tf b/terraform/modules/storage/database/mongo/inputs.tf index f8c6cc0b7..2292638fa 100644 --- a/terraform/modules/storage/database/mongo/inputs.tf +++ b/terraform/modules/storage/database/mongo/inputs.tf @@ -18,7 +18,3 @@ variable "mongodb_params" { windows = bool }) } - -variable "partition_list" { - type = map(any) -} diff --git a/terraform/modules/storage/database/mongo/main.tf b/terraform/modules/storage/database/mongo/main.tf index 388b9ca2d..a3fd30885 100644 --- a/terraform/modules/storage/database/mongo/main.tf +++ b/terraform/modules/storage/database/mongo/main.tf @@ -49,11 +49,3 @@ resource "null_resource" "init_replica" { } depends_on = [time_sleep.wait] } - -resource "null_resource" "partitions_in_db" { - for_each = var.partition_list - provisioner "local-exec" { - command = "${local.prefix_run} --eval 'db.PartitionData.insertOne(${jsonencode(each.value)})'" - } - depends_on = [null_resource.init_replica] -} diff --git a/terraform/modules/storage/database/mongo/outputs.tf b/terraform/modules/storage/database/mongo/outputs.tf index 364c42c83..4a4797f2d 100644 --- a/terraform/modules/storage/database/mongo/outputs.tf +++ b/terraform/modules/storage/database/mongo/outputs.tf @@ -11,5 +11,5 @@ output "generated_env_vars" { "MongoDB__ReplicaSet" = "${var.mongodb_params.replica_set_name}" } - depends_on = [null_resource.partitions_in_db] + depends_on = [null_resource.init_replica] } diff --git a/terraform/modules/submitter/inputs.tf b/terraform/modules/submitter/inputs.tf index 0db8bf044..557d2ea7b 100644 --- a/terraform/modules/submitter/inputs.tf +++ b/terraform/modules/submitter/inputs.tf @@ -29,3 +29,7 @@ variable "log_driver" { }) } +variable "container_init" { + type = bool +} + diff --git a/terraform/modules/submitter/locals.tf b/terraform/modules/submitter/locals.tf index 60d5c5bdf..7351c19b5 100644 --- a/terraform/modules/submitter/locals.tf +++ b/terraform/modules/submitter/locals.tf @@ -1,6 +1,13 @@ locals { - env = [ + common_env = [ "Submitter__DefaultPartition=TestPartition0", ] + control_plane_env = [ + "InitServices__InitDatabase=${!var.container_init}", + ] + init_env = [ + "InitServices__StopAfterInit=true", + "Serilog__Properties__Application=ArmoniK.Control.Init", + ] gen_env = [for k, v in var.generated_env_vars : "${k}=${v}"] } diff --git a/terraform/modules/submitter/main.tf b/terraform/modules/submitter/main.tf index 5f6f1c2e4..24a81a011 100644 --- a/terraform/modules/submitter/main.tf +++ b/terraform/modules/submitter/main.tf @@ -3,6 +3,31 @@ resource "docker_image" "submitter" { keep_locally = true } +resource "docker_container" "init" { + name = "${var.container_name}_init" + image = docker_image.submitter.image_id + count = var.container_init ? 1 : 0 + + networks_advanced { + name = var.network + } + + env = concat(local.common_env, local.gen_env, local.init_env) + + log_driver = var.log_driver.name + log_opts = var.log_driver.log_opts + must_run = false + + dynamic "mounts" { + for_each = var.volumes + content { + type = "volume" + target = mounts.value + source = mounts.key + } + } +} + resource "docker_container" "submitter" { name = var.container_name image = docker_image.submitter.image_id @@ -11,7 +36,7 @@ resource "docker_container" "submitter" { name = var.network } - env = concat(local.env, local.gen_env) + env = concat(local.common_env, local.gen_env, local.control_plane_env) log_driver = var.log_driver.name log_opts = var.log_driver.log_opts @@ -34,4 +59,6 @@ resource "docker_container" "submitter" { source = mounts.key } } + + depends_on = [docker_container.init] } diff --git a/terraform/variables.tf b/terraform/variables.tf index ea1eb649a..7dab51033 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -137,7 +137,7 @@ variable "compute_plane" { variable "partition_data" { description = "Template to create multiple partitions" type = object({ - _id = optional(string, "TestPartition") + PartitionId = optional(string, "TestPartition") Priority = optional(number, 1) PodReserved = optional(number, 50) PodMax = optional(number, 100) @@ -260,3 +260,8 @@ variable "tracing_ingestion_ports" { default = { } } + +variable "container_init" { + type = bool + default = true +}