diff --git a/.docs/content/1.concepts/3.authentication.md b/.docs/content/1.concepts/3.authentication.md index 20e4aad77..c5e5f1ade 100644 --- a/.docs/content/1.concepts/3.authentication.md +++ b/.docs/content/1.concepts/3.authentication.md @@ -23,11 +23,11 @@ ArmoniK allows users to impersonate other users by adding an impersonation heade ArmoniK uses a User-Role-Permission based approach to handle authorization. Each user in the database can have a set of **Roles**. Each role contains a set of **Permissions**. A user cannot receive permissions directly, instead roles containing the permissions have to be created and given to the user. A permission is defined as a string in a specific format. The current version handles the following types of permissions : -|Format|Example|Parameters|Description| ----|---|---|---| -``General:Impersonate:``|``General:Impersonate:Monitoring``|**Rolename**: Name of a role|Grants the right to impersonate a user with the role named \. See [Impersonation](#impersonation) for details| -|``:``|``Submitter:CreateSession``|**Service**: Name of an ArmoniK web service
**Name**: Name of the endpoint|Grants the right to use the endpoint named \ of the service named \| -|``::``|``Submitter:CancelSession:Self``|**Service**: Name of an ArmoniK web service
**Name**: Name of the endpoint
**Target**: Target or scope of the permission|Same as ``:`` as ```` is currently unused| +| Format | Example | Parameters | Description | +|------------------------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| ``General:Impersonate:`` | ``General:Impersonate:Monitoring`` | **Rolename**: Name of a role | Grants the right to impersonate a user with the role named \. See [Impersonation](#impersonation) for details | +| ``:`` | ``Submitter:CreateSession`` | **Service**: Name of an ArmoniK web service
**Name**: Name of the endpoint | Grants the right to use the endpoint named \ of the service named \ | +| ``::`` | ``Submitter:CancelSession:Self`` | **Service**: Name of an ArmoniK web service
**Name**: Name of the endpoint
**Target**: Target or scope of the permission | Same as ``:`` as ```` is currently unused | ## User authorization @@ -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 by the administrator during deployment. 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 - - Roles : list of objectIds, each matching the _id field in RoleData of the roles given to the user -- RoleData + - Name : Unique user name + - Roles : list of role names, each matching the Name field of the roles given to the user +- 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 need 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 authentication 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 environment variable: + +```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 called "User1", use the following environment variable: ```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 : +To insert an entry matching all certificates with Common Name "CN1" associated with the User called "User1", use the following environment variable: ```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 9ab644eeb..d90d32f6d 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # 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/AuthenticationTable.cs b/Adaptors/MongoDB/src/AuthenticationTable.cs index fc0672ed8..b402bf6ba 100644 --- a/Adaptors/MongoDB/src/AuthenticationTable.cs +++ b/Adaptors/MongoDB/src/AuthenticationTable.cs @@ -183,7 +183,7 @@ await authCollectionProvider_.Init(cancellationToken) } /// - public async Task GetIdentityFromUserAsync(string? id, + public async Task GetIdentityFromUserAsync(int? id, string? username, CancellationToken cancellationToken = default) { 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..b0d9a5e51 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.LogWarning(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; @@ -158,9 +171,9 @@ await model.InitializeIndexesAsync(session, catch (Exception ex) { lastException = ex; - logger.LogDebug(ex, - "Retrying to Initialize indexes for {CollectionName} collection", - model.CollectionName); + logger.LogWarning(ex, + "Retrying to Initialize indexes for {CollectionName} collection", + model.CollectionName); await Task.Delay(1000 * indexRetry, cancellationToken) .ConfigureAwait(false); @@ -182,9 +195,9 @@ await model.ShardCollectionAsync(session, catch (Exception ex) { lastException = ex; - logger.LogDebug(ex, - "Retrying to shard {CollectionName} collection", - model.CollectionName); + logger.LogWarning(ex, + "Retrying to shard {CollectionName} collection", + model.CollectionName); await Task.Delay(1000 * indexRetry, cancellationToken) .ConfigureAwait(false); @@ -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.LogWarning(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..a44d0188f 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; @@ -38,11 +40,9 @@ static AuthDataModelMapping() BsonClassMap.RegisterClassMap(cm => { cm.MapIdProperty(nameof(AuthData.AuthId)) - .SetIsRequired(true) - .SetSerializer(IdSerializer.Instance); + .SetIsRequired(true); cm.MapProperty(nameof(AuthData.UserId)) - .SetIsRequired(true) - .SetSerializer(IdSerializer.Instance); + .SetIsRequired(true); cm.MapProperty(nameof(AuthData.Cn)) .SetIsRequired(true); cm.MapProperty(nameof(AuthData.Fingerprint)) @@ -78,7 +78,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/AuthMongoUtils.cs b/Adaptors/MongoDB/src/Table/DataModel/Auth/AuthMongoUtils.cs deleted file mode 100644 index 882855f13..000000000 --- a/Adaptors/MongoDB/src/Table/DataModel/Auth/AuthMongoUtils.cs +++ /dev/null @@ -1,156 +0,0 @@ -// 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 MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; - -namespace ArmoniK.Core.Adapters.MongoDB.Table.DataModel.Auth; - -/// -/// Extension class for MongoDB string conversion -/// -public static class AuthMongoUtils -{ - /// - /// Extension method to transform a string to an objectId - /// - /// The string to convert - /// Converted string - public static string ToOidString(this string value) - => IdSerializer.ToValidIdString(value); -} - -/// -/// Serializer class to/from Object to/from string -/// -public class IdSerializer : SerializerBase -{ - /// - /// Singleton instance of the serializer - /// - public static readonly IdSerializer Instance = new(); - - /// - /// Method used by the MongoDB driver to deserialize an ObjectID to string - /// - /// Deserialization context - /// Deserialization arguments - /// ObjectID as a string - public override string Deserialize(BsonDeserializationContext context, - BsonDeserializationArgs args) - => Deserialize(context.Reader.ReadObjectId()); - - /// - /// Method to deserialize an objectId into a string - /// - /// The ObjectId - /// ObjectId as a string - public static string Deserialize(ObjectId id) - => id.ToString(); - - /// - /// Method to serialize an string into an ObjectId - /// - /// the string, must be a 24 length hexstring - /// ObjectId - public static ObjectId Serialize(string value) - => ObjectId.Parse(value); - - /// - /// Method used by the MongoDB driver to serialize a string to an ObjectID - /// - /// Serialization context - /// Serialization arguments - /// String to serialize - public override void Serialize(BsonSerializationContext context, - BsonSerializationArgs args, - string value) - => context.Writer.WriteObjectId(Serialize(value)); - - /// - /// Converts any string to a valid hex string to be used as a MongoDB ID - /// - /// the string to convert - /// String as an ObjectId string - public static string ToValidIdString(string value) - { - var hash = value.GetHashCode(); - return ObjectId.Parse(Convert.ToHexString(new List - { - hash, - hash, - hash, - }.SelectMany(BitConverter.GetBytes) - .ToArray())) - .ToString(); - } -} - -/// -/// Serializer to handle arrays of strings and arrays of ObjectIds -/// -public class IdArraySerializer : SerializerBase -{ - /// - /// Serializer singleton instance - /// - public static readonly IdArraySerializer Instance = new(); - - /// - /// Method used by the MongoDB driver to deserialize an array of ObjectIDs to an array of strings - /// - /// Deserialization context - /// Deserialization arguments - /// Array of strings deserialized from ObjectIds - public override string[] Deserialize(BsonDeserializationContext context, - BsonDeserializationArgs args) - { - var res = new List(); - context.Reader.ReadStartArray(); - while (context.Reader.ReadBsonType() != BsonType.EndOfDocument) - { - res.Add(IdSerializer.Deserialize(context.Reader.ReadObjectId())); - } - - context.Reader.ReadEndArray(); - return res.ToArray(); - } - - /// - /// Method used by the MongoDB driver to serialize an array of strings to an array of ObjectIDs - /// - /// Serialization context - /// Serialization arguments - /// Array of strings String to serialize - public override void Serialize(BsonSerializationContext context, - BsonSerializationArgs args, - string[] value) - { - context.Writer.WriteStartArray(); - foreach (var s in value) - { - context.Writer.WriteObjectId(IdSerializer.Serialize(s)); - } - - context.Writer.WriteEndArray(); - } -} diff --git a/Adaptors/MongoDB/src/Table/DataModel/Auth/PipelineIntermediates.cs b/Adaptors/MongoDB/src/Table/DataModel/Auth/PipelineIntermediates.cs index 6df1dc1b7..3c401d7dc 100644 --- a/Adaptors/MongoDB/src/Table/DataModel/Auth/PipelineIntermediates.cs +++ b/Adaptors/MongoDB/src/Table/DataModel/Auth/PipelineIntermediates.cs @@ -19,7 +19,6 @@ using ArmoniK.Core.Common.Auth.Authentication; -using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace ArmoniK.Core.Adapters.MongoDB.Table.DataModel.Auth; @@ -34,8 +33,8 @@ namespace ArmoniK.Core.Adapters.MongoDB.Table.DataModel.Auth; /// List of users that have the id UserId [BsonIgnoreExtraElements] public record AuthDataAfterLookup([property: BsonId] - ObjectId AuthId, - ObjectId UserId, + int AuthId, + int UserId, string Cn, string Fingerprint, UserData[] UserData); @@ -48,7 +47,7 @@ public record AuthDataAfterLookup([property: BsonId] /// List of roles of the user [BsonIgnoreExtraElements] public record UserDataAfterLookup([property: BsonId] - ObjectId UserId, + int UserId, string Username, IEnumerable Roles); @@ -60,7 +59,7 @@ public record UserDataAfterLookup([property: BsonId] /// User's roles /// User's permissions public record MongoAuthResult([property: BsonId] - ObjectId Id, + int Id, string Username, IEnumerable Roles, IEnumerable Permissions) @@ -70,7 +69,7 @@ public record MongoAuthResult([property: BsonId] /// /// UserAuthenticationResult from this object public UserAuthenticationResult ToUserAuthenticationResult() - => new(IdSerializer.Deserialize(Id), + => new(Id, Username, Roles, Permissions); diff --git a/Adaptors/MongoDB/src/Table/DataModel/Auth/RoleDataModelMapping.cs b/Adaptors/MongoDB/src/Table/DataModel/Auth/RoleDataModelMapping.cs index 5da24a1cc..4d0fc131b 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; @@ -38,8 +40,7 @@ static RoleDataModelMapping() BsonClassMap.RegisterClassMap(cm => { cm.MapIdProperty(nameof(RoleData.RoleId)) - .SetIsRequired(true) - .SetSerializer(IdSerializer.Instance); + .SetIsRequired(true); cm.MapProperty(nameof(RoleData.RoleName)) .SetIsRequired(true); cm.MapProperty(nameof(RoleData.Permissions)) @@ -74,7 +75,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..a25cd8c8e 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; @@ -38,13 +40,11 @@ static UserDataModelMapping() BsonClassMap.RegisterClassMap(cm => { cm.MapIdProperty(nameof(UserData.UserId)) - .SetIsRequired(true) - .SetSerializer(IdSerializer.Instance); + .SetIsRequired(true); cm.MapProperty(nameof(UserData.Username)) .SetIsRequired(true); cm.MapProperty(nameof(UserData.Roles)) .SetIgnoreIfDefault(true) - .SetSerializer(IdArraySerializer.Instance) .SetDefaultValue(Array.Empty()); cm.SetIgnoreExtraElements(true); cm.MapCreator(model => new UserData(model.UserId, @@ -74,7 +74,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/AuthenticationTableTest.cs b/Adaptors/MongoDB/tests/AuthenticationTableTest.cs index 2e6a4d5e0..cabb69c51 100644 --- a/Adaptors/MongoDB/tests/AuthenticationTableTest.cs +++ b/Adaptors/MongoDB/tests/AuthenticationTableTest.cs @@ -17,7 +17,6 @@ using System; -using ArmoniK.Core.Adapters.MongoDB.Table.DataModel.Auth; using ArmoniK.Core.Common.Auth.Authentication; using ArmoniK.Core.Common.Tests.TestBase; @@ -44,8 +43,6 @@ static AuthenticationTableTest() BsonClassMap.RegisterClassMap(cm => { cm.MapIdProperty(nameof(UserAuthenticationResult.Id)) - .SetSerializer(IdSerializer.Instance) - .SetShouldSerializeMethod(_ => true) .SetIsRequired(true); cm.MapProperty(nameof(UserAuthenticationResult.Username)) .SetIsRequired(true); diff --git a/Adaptors/MongoDB/tests/BsonSerializerTest.cs b/Adaptors/MongoDB/tests/BsonSerializerTest.cs index 0f5d39d83..269da283d 100644 --- a/Adaptors/MongoDB/tests/BsonSerializerTest.cs +++ b/Adaptors/MongoDB/tests/BsonSerializerTest.cs @@ -239,12 +239,12 @@ public void SerializeTaskDataModel() [Test] public void SerializeUserDataModel() { - var udm = new UserData("UserId".ToOidString(), + var udm = new UserData(0, "Username", new[] { - "RoleId1".ToOidString(), - "RoleId2".ToOidString(), + 0, + 1, }); var serialized = udm.ToBson(); @@ -267,7 +267,7 @@ public void SerializeUserDataModel() [Test] public void SerializeRoleDataModel() { - var rdm = new RoleData("RoleId".ToOidString(), + var rdm = new RoleData(0, "RoleName", new[] { @@ -295,8 +295,8 @@ public void SerializeRoleDataModel() [Test] public void SerializeAuthDataModel() { - var adm = new AuthData("AuthId".ToOidString(), - "UserId".ToOidString(), + var adm = new AuthData(1, + 1, "CN", "Fingerprint"); var serialized = adm.ToBson(); @@ -319,7 +319,7 @@ public void SerializeAuthDataModel() [Test] public void SerializeUserAuthenticationResult() { - var uirm = new UserAuthenticationResult("Id".ToOidString(), + var uirm = new UserAuthenticationResult(0, "Username", new[] { 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/Auth/Authentication/AuthData.cs b/Common/src/Auth/Authentication/AuthData.cs index 2c50db4c4..a04ccaf06 100644 --- a/Common/src/Auth/Authentication/AuthData.cs +++ b/Common/src/Auth/Authentication/AuthData.cs @@ -27,7 +27,7 @@ namespace ArmoniK.Core.Common.Auth.Authentication; /// fingerprint of the certificate. If null, this entry matches with every certificates /// matching the Common Name /// -public record AuthData(string AuthId, - string UserId, +public record AuthData(int AuthId, + int UserId, string Cn, string? Fingerprint); diff --git a/Common/src/Auth/Authentication/Authenticator.cs b/Common/src/Auth/Authentication/Authenticator.cs index 6a2df81b6..250176ee5 100644 --- a/Common/src/Auth/Authentication/Authenticator.cs +++ b/Common/src/Auth/Authentication/Authenticator.cs @@ -242,7 +242,9 @@ protected override async Task HandleAuthenticateAsync() { var prevIdentity = identity; identity = await GetImpersonatedIdentityAsync(identity, - impersonationId, + impersonationId is null + ? null + : int.Parse(impersonationId), impersonationUsername) .ConfigureAwait(false); logger_.LogInformation("User with id {userId} and name {userName} impersonated the user with id {impersonatedId} and name {impersonatedName}. Authentication key : {keyHash}", @@ -316,7 +318,7 @@ protected override async Task HandleAuthenticateAsync() /// or the impersonating user doesn't have the permissions to impersonate the specified user /// public async Task GetImpersonatedIdentityAsync(ClaimsPrincipal baseIdentity, - string? impersonationId, + int? impersonationId, string? impersonationUsername, CancellationToken cancellationToken = default) { diff --git a/Common/src/Auth/Authentication/IAuthenticationTable.cs b/Common/src/Auth/Authentication/IAuthenticationTable.cs index e9a8d8e3f..9d19396c4 100644 --- a/Common/src/Auth/Authentication/IAuthenticationTable.cs +++ b/Common/src/Auth/Authentication/IAuthenticationTable.cs @@ -47,7 +47,7 @@ public interface IAuthenticationTable : IInitializable /// User name /// Cancellation token /// User authentication data matching the id, if not null, otherwise the username, null if not found - public Task GetIdentityFromUserAsync(string? id, + public Task GetIdentityFromUserAsync(int? id, string? username, CancellationToken cancellationToken = default); diff --git a/Common/src/Auth/Authentication/RoleData.cs b/Common/src/Auth/Authentication/RoleData.cs index 2ed6a9c3f..28c88baae 100644 --- a/Common/src/Auth/Authentication/RoleData.cs +++ b/Common/src/Auth/Authentication/RoleData.cs @@ -23,6 +23,6 @@ namespace ArmoniK.Core.Common.Auth.Authentication; /// Role Id /// Role Name /// Permissions list, as strings -public record RoleData(string RoleId, +public record RoleData(int RoleId, string RoleName, string[] Permissions); diff --git a/Common/src/Auth/Authentication/UserAuthenticationResult.cs b/Common/src/Auth/Authentication/UserAuthenticationResult.cs index af00b52ec..dc4c8bd44 100644 --- a/Common/src/Auth/Authentication/UserAuthenticationResult.cs +++ b/Common/src/Auth/Authentication/UserAuthenticationResult.cs @@ -27,7 +27,7 @@ namespace ArmoniK.Core.Common.Auth.Authentication; /// User name /// User roles /// User permissions -public record UserAuthenticationResult(string Id, +public record UserAuthenticationResult(int Id, string Username, IEnumerable Roles, IEnumerable Permissions) @@ -36,7 +36,7 @@ public record UserAuthenticationResult(string Id, /// Creates an empty result /// public UserAuthenticationResult() - : this("", + : this(0, "", Array.Empty(), Array.Empty()) diff --git a/Common/src/Auth/Authentication/UserData.cs b/Common/src/Auth/Authentication/UserData.cs index 8b855dbde..719146dd1 100644 --- a/Common/src/Auth/Authentication/UserData.cs +++ b/Common/src/Auth/Authentication/UserData.cs @@ -23,6 +23,6 @@ namespace ArmoniK.Core.Common.Auth.Authentication; /// User Id /// User name /// User roles -public record UserData(string UserId, - string Username, - string[] Roles); +public record UserData(int UserId, + string Username, + int[] Roles); diff --git a/Common/src/Auth/Authentication/UserIdentity.cs b/Common/src/Auth/Authentication/UserIdentity.cs index 25f247e60..30993f6a2 100644 --- a/Common/src/Auth/Authentication/UserIdentity.cs +++ b/Common/src/Auth/Authentication/UserIdentity.cs @@ -63,7 +63,7 @@ public UserIdentity(UserAuthenticationResult userAuth, /// /// User Id /// - public string UserId { get; set; } + public int UserId { get; set; } /// /// Transforms a UserAuthenticationResult into a list of claims to be used in an ClaimsIdentity 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..12f6f4f14 --- /dev/null +++ b/Common/src/Injection/Options/Database/InitDatabase.cs @@ -0,0 +1,109 @@ +// 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, + role.Name, + role.Permissions.ToArray())) + .AsICollection(); + + var roleDic = Roles.ToDictionary(data => data.RoleName, + data => data.RoleId); + + Users = initServices.Authentication.Users.Select(User.FromJson) + .OrderBy(user => user.Name) + .Select((user, + i) => new UserData(i, + user.Name, + user.Roles.Select(roleName => roleDic[roleName]) + .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, + 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/Auth/AuthenticationIntegrationTest.cs b/Common/tests/Auth/AuthenticationIntegrationTest.cs index 6ee71164c..ed8f6a5de 100644 --- a/Common/tests/Auth/AuthenticationIntegrationTest.cs +++ b/Common/tests/Auth/AuthenticationIntegrationTest.cs @@ -289,7 +289,7 @@ public enum ImpersonationType NoImpersonate, } - public const string AllRightsId = "AllRightsId"; + public const int AllRightsId = 0; public const string AllRightsUsername = "AllRightsUsername"; public const string AllRightsRole = "AllRights"; @@ -313,7 +313,7 @@ public enum ImpersonationType ServicesPermissions.PermissionsLists[ServicesPermissions.All], Authenticator.SchemeName), // No Rights - new("NoRightsId1", + new(1, "NoRightsUsername1", new[] { @@ -327,7 +327,7 @@ public enum ImpersonationType Array.Empty(), Authenticator.SchemeName), // Can impersonate - new("CanImpersonateId1", + new(2, "CanImpersonateUsername1", new[] { @@ -346,14 +346,14 @@ public enum ImpersonationType }, Authenticator.SchemeName), // Has no certificate - new("NoCertificateId", + new(3, "NoCertificateUsername", Array.Empty(), Array.Empty(), Array.Empty(), null), // Has half of the permissions - new("SomeRightsId", + new(4, "SomeRightsUsername", new[] { @@ -369,7 +369,7 @@ public enum ImpersonationType index) => index % 2 == 0), Authenticator.SchemeName), // Has the other half of the permissions - new("OtherRightsId", + new(5, "OtherRightsUsername", new[] { @@ -422,9 +422,9 @@ public static Metadata GetHeaders(IdentityIndex index, { headers.Add(AuthenticatorOptions.DefaultAuth.ImpersonationIdHeader, (int)impersonate < 0 - ? "DoesntExist" + ? "404" : Identities[(int)impersonate] - .UserId); + .UserId.ToString()); } else if (impersonationType == ImpersonationType.ImpersonateUsername) { diff --git a/Common/tests/Auth/MockAuthenticationTable.cs b/Common/tests/Auth/MockAuthenticationTable.cs index 643202abe..2ab557714 100644 --- a/Common/tests/Auth/MockAuthenticationTable.cs +++ b/Common/tests/Auth/MockAuthenticationTable.cs @@ -46,7 +46,7 @@ public Task Init(CancellationToken cancellationToken) fingerprint)) ?.ToUserAuthenticationResult()); - public Task GetIdentityFromUserAsync(string? id, + public Task GetIdentityFromUserAsync(int? id, string? username, CancellationToken cancellationToken) => Task.FromResult(identities_.Find(i => id is not null diff --git a/Common/tests/Auth/MockIdentity.cs b/Common/tests/Auth/MockIdentity.cs index 7a8df21bb..585c88e9d 100644 --- a/Common/tests/Auth/MockIdentity.cs +++ b/Common/tests/Auth/MockIdentity.cs @@ -27,7 +27,7 @@ public class MockIdentity : UserIdentity { public readonly IEnumerable Certificates; - public MockIdentity(string userId, + public MockIdentity(int userId, string username, IEnumerable certificates, IEnumerable roles, 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 8deee3be2..c9aa69dce 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; @@ -159,6 +161,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 70e5794bb..f013423cf 100644 --- a/Common/tests/TestBase/AuthenticationTableTestBase.cs +++ b/Common/tests/TestBase/AuthenticationTableTestBase.cs @@ -21,10 +21,11 @@ using System.Threading; using System.Threading.Tasks; -using ArmoniK.Core.Adapters.MongoDB.Table.DataModel.Auth; 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; @@ -38,6 +39,16 @@ public class AuthenticationTableTestBase [SetUp] 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()) @@ -63,11 +74,42 @@ private static bool CheckForSkipSetup() return category is "SkipSetUp"; } + 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 + { + "catEnv: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 { - new("RoleId1".ToOidString(), + new(11, "Role1", new[] { @@ -75,7 +117,7 @@ static AuthenticationTableTestBase() "category1:name2", "category2:name3", }), - new("RoleId2".ToOidString(), + new(12, "Role2", new[] { @@ -83,7 +125,7 @@ static AuthenticationTableTestBase() "category1:name2:" + PermissionScope.AllUsersScope, "category2:name4", }), - new("RoleId3".ToOidString(), + new(13, "Role3", new[] { @@ -91,20 +133,20 @@ static AuthenticationTableTestBase() "category4:name2", "category5:name3", }), - new("RoleId4".ToOidString(), + new(14, "Role4", Array.Empty()), }; Users = new List { - new("UserId1".ToOidString(), + new(21, "User1", new[] { Roles[0] .RoleId, }), - new("UserId2".ToOidString(), + new(22, "User2", new[] { @@ -113,7 +155,7 @@ static AuthenticationTableTestBase() Roles[1] .RoleId, }), - new("UserId3".ToOidString(), + new(23, "User3", new[] { @@ -122,60 +164,60 @@ static AuthenticationTableTestBase() Roles[2] .RoleId, }), - new("UserId4".ToOidString(), + new(24, "User4", new[] { Roles[0] .RoleId, - "RoleIdDontExist".ToOidString(), + 1000, // this one should not exist }), - new("UserId5".ToOidString(), + new(25, "User5", - Array.Empty()), + Array.Empty()), }; Auths = new List { - new("AuthId1".ToOidString(), + new(31, Users[0] .UserId, "CNUser1", "Fingerprint1"), - new("AuthId2".ToOidString(), + new(32, Users[1] .UserId, "CNUser2", "Fingerprint2"), - new("AuthId3".ToOidString(), + new(33, Users[1] .UserId, "CNUser3", "Fingerprint3"), - new("AuthId4".ToOidString(), + new(34, Users[2] .UserId, "CNUser4", "Fingerprint4"), - new("AuthId5".ToOidString(), + new(35, Users[3] .UserId, "CNUser5", "Fingerprint5"), - new("AuthId6".ToOidString(), - "UserIdDontExist".ToOidString(), + new(36, + 1000, // this user should not exist "CNUser6", "Fingerprint6"), - new("AuthId7".ToOidString(), + new(37, Users[1] .UserId, "CNUser2", "Fingerprint7"), - new("AuthId8".ToOidString(), + new(38, Users[2] .UserId, "CNUserCommon", null), - new("AuthId9".ToOidString(), + new(39, Users[3] .UserId, "CNUser2", @@ -320,15 +362,15 @@ public void GetIdentityFromIdShouldSucceed(int id, ident.Username); } - [TestCase("UserIdDontExist")] - public void GetIdentityFromIdShouldFail(string id) + [TestCase(1000)] + public void GetIdentityFromIdShouldFail(int id) { if (!RunTests) { return; } - Assert.IsNull(AuthenticationTable!.GetIdentityFromUserAsync(id.ToOidString(), + Assert.IsNull(AuthenticationTable!.GetIdentityFromUserAsync(id, null) .Result); } @@ -378,6 +420,9 @@ public void GetIdentityFromNameShouldFail(string name) [TestCase("User2", "Role1", true)] + [TestCase("UserEnv", + "RoleEnv", + true)] [TestCase("User2", "RoleDontExist", false)] @@ -413,6 +458,9 @@ public void UserHasRoleShouldMatch(string username, [TestCase("User2", "category1:name2:" + PermissionScope.AllUsersScope, true)] + [TestCase("UserEnv", + "catEnv:PermEnv", + true)] public void UserHasClaimShouldMatch(string username, string claim, bool hasClaim) 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 +}