コード例 #1
0
        GetDeploymentSetup(DeploymentTarget deploymentTarget)
        {
            var app = await _applicationService.Get(deploymentTarget.ApplicationName);

            if (app == null)
            {
                throw new Exception($"Can't find application [{deploymentTarget.ApplicationName}]. Check the correctness of the manifest.");
            }

            var env = await _environmentService.Get(app.Id, deploymentTarget.EnvironmentName);

            if (env == null)
            {
                throw new Exception($"Can't find environment [{deploymentTarget.EnvironmentName}] for application [{deploymentTarget.ApplicationName}]. Check the correctness of the manifest.");
            }

            var deploymentStrategies = await _client.ListDeploymentStrategiesAsync(new ListDeploymentStrategiesRequest());

            var deploymentStrategy =
                deploymentStrategies.Items.FirstOrDefault(x => x.Name == deploymentTarget.DeploymentStrategy);

            if (deploymentStrategy == null)
            {
                throw new Exception($"Can't find deployment strategy [{deploymentTarget.DeploymentStrategy}]. Check the correctness of the manifest.");
            }

            return(app, env, deploymentStrategy);
        }
コード例 #2
0
        /// <summary>
        /// Deploys a registered model
        /// </summary>
        /// <param name="deploymentTarget"></param>
        /// <param name="registeredModel"></param>
        /// <param name="deployedBy"></param>
        /// <returns>A deployment</returns>
        /// <returns></returns>
        public async Task <Deployment> DeployModelAsync(DeploymentTarget deploymentTarget, RegisteredModel registeredModel, string deployedBy)
        {
            var experiment    = this.experimentRepository.GetExperiment(registeredModel.ExperimentId);
            var deploymentUri = await this.modelRepository.DeployModelAsync(deploymentTarget, registeredModel, experiment);

            return(await this.deploymentRepository.CreateDeploymentAsync(deploymentTarget, registeredModel, deployedBy, deploymentUri));
        }
コード例 #3
0
        public async Task DeployModelAsync_GivenADeployedModelAlreadyExists_ShouldOverwriteExistingModel()
        {
            //Arrange
            var client = new HttpClient();

            var deploymentTarget = new DeploymentTarget("Test");

            var firstModelUri = await DeployModelAsync(deploymentTarget);

            var model1 = await client.GetAsync(firstModelUri);

            var firstModelUpdateTime = model1.Content.Headers.LastModified;

            Thread.Sleep(60000);

            //Act
            var uri = await DeployModelAsync(deploymentTarget);

            //Assert
            var model2 = await client.GetAsync(uri);

            var secondModelUpdateTime = model2.Content.Headers.LastModified;

            firstModelUpdateTime.Value.Ticks.Should().BeLessThan(secondModelUpdateTime.Value.Ticks);
        }
コード例 #4
0
        public async Task SeedAsync(CancellationToken cancellationToken)
        {
            var testTarget = new DeploymentTarget("TestTarget",
                                                  "Test target",
                                                  "MilouDeployerWebTest",
                                                  allowExplicitPreRelease: false,
                                                  autoDeployEnabled: true,
                                                  targetDirectory: Environment.GetEnvironmentVariable("TestDeploymentTargetPath"),
                                                  uri: Environment.GetEnvironmentVariable("TestDeploymentUri"),
                                                  emailNotificationAddresses: new StringValues("*****@*****.**"),
                                                  enabled: true);

            var createTarget = new CreateTarget(testTarget.Id, testTarget.Name);
            await _mediator.Send(createTarget, cancellationToken);

            var updateDeploymentTarget = new UpdateDeploymentTarget(testTarget.Id,
                                                                    testTarget.AllowPreRelease,
                                                                    testTarget.Url,
                                                                    testTarget.PackageId,
                                                                    autoDeployEnabled: testTarget.AutoDeployEnabled,
                                                                    targetDirectory: testTarget.TargetDirectory,
                                                                    enabled: true);

            await _mediator.Send(updateDeploymentTarget,
                                 cancellationToken);
        }
コード例 #5
0
 public DeploymentTargetViewOutputModel(
     [NotNull] DeploymentTarget target,
     [NotNull] IReadOnlyCollection <StringPair> configurationPairs)
 {
     Target             = target ?? throw new ArgumentNullException(nameof(target));
     ConfigurationPairs = configurationPairs ?? throw new ArgumentNullException(nameof(configurationPairs));
 }
コード例 #6
0
        private static DeploymentTarget MapDataToTarget(DeploymentTargetData deploymentTargetData)
        {
            if (deploymentTargetData is null)
            {
                return(null);
            }

            var deploymentTargetAsync = new DeploymentTarget(
                deploymentTargetData.Id,
                deploymentTargetData.Name,
                deploymentTargetData.PackageId ?? Core.Constants.NotAvailable,
                deploymentTargetData.PublishSettingsXml,
                deploymentTargetData.AllowExplicitPreRelease,
                uri: deploymentTargetData.Url?.ToString(),
                nuGetConfigFile: deploymentTargetData.NuGetConfigFile,
                nuGetPackageSource: deploymentTargetData.NuGetPackageSource,
                iisSiteName: deploymentTargetData.IisSiteName,
                autoDeployEnabled: deploymentTargetData.AutoDeployEnabled,
                targetDirectory: deploymentTargetData.TargetDirectory,
                webConfigTransform: deploymentTargetData.WebConfigTransform,
                excludedFilePatterns: deploymentTargetData.ExcludedFilePatterns,
                enabled: deploymentTargetData.Enabled);

            return(deploymentTargetAsync);
        }
コード例 #7
0
        public async Task <DeploymentTarget> GetDeploymentTargetAsync(
            [NotNull] string deploymentTargetId,
            CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(deploymentTargetId))
            {
                throw new ArgumentException("Value cannot be null or whitespace.", nameof(deploymentTargetId));
            }

            using (IQuerySession session = _documentStore.QuerySession())
            {
                try
                {
                    DeploymentTargetData deploymentTargetData = await session.Query <DeploymentTargetData>()
                                                                .SingleOrDefaultAsync(target =>
                                                                                      target.Id.Equals(deploymentTargetId, StringComparison.OrdinalIgnoreCase),
                                                                                      cancellationToken);

                    DeploymentTarget deploymentTarget = MapDataToTarget(deploymentTargetData);

                    return(deploymentTarget);
                }
                catch (Exception ex) when(!ex.IsFatal())
                {
                    _logger.Warning(ex, "Could not get deployment target with id {Id}", deploymentTargetId);
                    return(DeploymentTarget.None);
                }
            }
        }
コード例 #8
0
        ///<inheritdoc cref="IDeploymentRepository"/>
        public async Task <DeploymentTarget> CreateDeploymentTargetAsync(string deploymentTargetName, bool isProduction = false)
        {
            if (string.IsNullOrEmpty(deploymentTargetName))
            {
                throw new ArgumentNullException("Deployment target name was not specified");
            }

            using var db = this.contextFactory.CreateDbContext();

            var existingDeploymentTarget = db.DeploymentTargets.FirstOrDefault(x => x.Name == deploymentTargetName);

            if (existingDeploymentTarget != null)
            {
                return(this.deploymentTargetResolver.BuildEntity(db, existingDeploymentTarget));
            }

            var deploymentTarget = new DeploymentTarget(deploymentTargetName)
            {
                CreatedDate  = clock.UtcNow,
                IsProduction = isProduction
            };

            db.DeploymentTargets.Add(deploymentTarget);

            await db.SaveChangesAsync();

            return(deploymentTarget);
        }
コード例 #9
0
        private async Task <AppVersion> GetAppVersionAsync(
            HttpResponseMessage response,
            DeploymentTarget target,
            IReadOnlyCollection <PackageVersion> filtered, CancellationToken cancellationToken)
        {
            if (response.Content.Headers.ContentType?.MediaType.Equals("application/json",
                                                                       StringComparison.OrdinalIgnoreCase) != true)
            {
                return(new AppVersion(target, "Response not JSON", filtered));
            }

            string json = await response.Content.ReadAsStringAsync();

            if (cancellationToken.IsCancellationRequested)
            {
                return(new AppVersion(target, "Timeout", filtered));
            }

            ConfigurationItems configuration =
                new JsonConfigurationSerializer().Deserialize(json);

            var nameValueCollection = new NameValueCollection();

            foreach (KeyValue configurationItem in configuration.Keys)
            {
                nameValueCollection.Add(configurationItem.Key, configurationItem.Value);
            }

            var appVersion = new AppVersion(target, new InMemoryKeyValueConfiguration(nameValueCollection), filtered);

            return(appVersion);
        }
コード例 #10
0
        public async Task GetDeploymentUri_GivenADeployedModel_ShouldReturnAValidUri()
        {
            //Arrange
            var runId      = Guid.NewGuid();
            var experiment = new Experiment("ExperimentName");

            await sut.UploadModelAsync(runId, @"Data/model.txt");

            var registeredModel = new RegisteredModel
            {
                RunId        = runId,
                ExperimentId = experiment.ExperimentId
            };

            var deploymentTarget = new DeploymentTarget("Test");
            await sut.DeployModelAsync(deploymentTarget, registeredModel, experiment);

            //Act
            var uri = sut.GetDeploymentUri(experiment, deploymentTarget);

            //Assert
            var client   = new HttpClient();
            var response = await client.GetAsync(uri);

            response.StatusCode.Should().Be(HttpStatusCode.OK);
            response.Content.Headers.ContentLength.Should().BeGreaterThan(0);
        }
コード例 #11
0
        private static string LogJobMetadata(
            DeploymentTask deploymentTask,
            DateTime start,
            DateTime end,
            Stopwatch stopwatch,
            ExitCode exitCode,
            DirectoryInfo deploymentJobsDirectory,
            DeploymentTarget deploymentTarget)
        {
            var metadata = new StringBuilder();

            metadata
            .Append("Started job ")
            .Append(deploymentTask.DeploymentTaskId)
            .Append(" at ")
            .AppendFormat("{0:O}", start)
            .Append(" and finished at ")
            .AppendFormat("{0:O}", end).AppendLine();

            metadata
            .Append("Total time ")
            .AppendFormat("{0:f}", stopwatch.Elapsed.TotalSeconds)
            .AppendLine(" seconds");

            metadata
            .Append("Package version: ")
            .Append(deploymentTask.SemanticVersion)
            .AppendLine();

            metadata
            .Append("Package id: ")
            .AppendLine(deploymentTask.PackageId);

            metadata
            .Append("Target id: ")
            .AppendLine(deploymentTask.DeploymentTargetId);

            if (deploymentTarget is null)
            {
                metadata.AppendLine("Deployment target not found");
            }
            else
            {
                metadata.Append("Publish settings file: ").AppendLine(deploymentTarget.PublishSettingFile);
                metadata.Append("Target directory: ").AppendLine(deploymentTarget.TargetDirectory);
                metadata.Append("Target URI: ").Append(deploymentTarget.Url).AppendLine();
            }

            metadata.Append("Exit code ").Append(exitCode).AppendLine();

            string metadataContent = metadata.ToString();

            string metadataFilePath = Path.Combine(deploymentJobsDirectory.FullName,
                                                   $"{deploymentTask.DeploymentTaskId}.metadata.txt");

            File.WriteAllText(metadataFilePath, metadataContent, Encoding.UTF8);

            return(metadataContent);
        }
コード例 #12
0
        public void GetConfigFromTargetWithoutEnvironment()
        {
            var target = new DeploymentTarget(new DeploymentTargetId("123"), "123", "abc");

            string?environmentConfig = target.GetEnvironmentConfiguration();

            Assert.Null(environmentConfig);
        }
コード例 #13
0
        private async Task <(ExitCode, DateTime)> RunDeploymentToolAsync(
            DeploymentTask deploymentTask,
            DirectoryInfo deploymentJobsDirectory,
            DeploymentTarget deploymentTarget,
            ILogger logger,
            CancellationToken cancellationToken = default)
        {
            string contentFilePath = GetMainLogFilePath(deploymentTask,
                                                        deploymentJobsDirectory);

            ExitCode exitCode;

            var logBuilder = new StringBuilder();

            LoggerConfiguration loggerConfiguration = new LoggerConfiguration()
                                                      .WriteTo.File(contentFilePath)
                                                      .WriteTo.DelegateSink(deploymentTask.Log)
                                                      .WriteTo.DelegateSink(message => logBuilder.AppendLine(message))
                                                      .WriteTo.Logger(logger);

            if (Debugger.IsAttached)
            {
                loggerConfiguration = loggerConfiguration.WriteTo.Debug(LogEventLevel.Verbose);
            }

            loggerConfiguration = loggerConfiguration.MinimumLevel.ControlledBy(_loggingLevelSwitch);

            using (Logger log = loggerConfiguration.CreateLogger())
            {
                if (logger.IsEnabled(LogEventLevel.Debug))
                {
                    logger.Debug(
                        "Running tool '{Deployer}' for deployment target '{DeploymentTarget}', package '{PackageId}' version {Version}",
                        _deployer,
                        deploymentTarget,
                        deploymentTask.PackageId,
                        deploymentTask.SemanticVersion.ToNormalizedString());
                }

                try
                {
                    exitCode = await _deployer.ExecuteAsync(deploymentTask, log, cancellationToken);
                }
                catch (Exception ex) when(!ex.IsFatal())
                {
                    _logger.Error(ex, "Failed to deploy task {DeploymentTask}", deploymentTask);
                    exitCode = ExitCode.Failure;
                }
            }

            DateTime finishedAtUtc = _customClock.UtcNow().UtcDateTime;

            await _mediator.Publish(
                new DeploymentFinishedNotification(deploymentTask, logBuilder.ToString(), finishedAtUtc),
                cancellationToken);

            return(exitCode, finishedAtUtc);
        }
コード例 #14
0
        public void GetConfigFromTargetWithOldEnvironment()
        {
            var target = new DeploymentTarget(new DeploymentTargetId("123"), "123", "abc",
                                              environmentConfiguration: "test");

            string?environmentConfig = target.GetEnvironmentConfiguration();

            Assert.Equal("test", environmentConfig);
        }
コード例 #15
0
        public void GetConfigFromTargetWithOtherEnvironment()
        {
            var target = new DeploymentTarget(new DeploymentTargetId("123"), "123", "abc",
                                              environmentType: new EnvironmentType("", "", PreReleaseBehavior.Allow));

            string?environmentConfig = target.GetEnvironmentConfiguration();

            Assert.Null(environmentConfig);
        }
コード例 #16
0
        public async Task <AppVersion> GetAppMetadataAsync(
            [NotNull] DeploymentTarget target,
            CancellationToken cancellationToken = default)
        {
            if (target == null)
            {
                throw new ArgumentNullException(nameof(target));
            }

            AppVersion appMetadata;

            using (var cancellationTokenSource =
                       new CancellationTokenSource(TimeSpan.FromSeconds(_monitorConfiguration.DefaultTimeoutInSeconds)))
            {
                if (_logger.IsEnabled(LogEventLevel.Verbose))
                {
                    cancellationTokenSource.Token.Register(() =>
                    {
                        _logger.Verbose("{Method} for {Target}, cancellation token invoked out after {Seconds} seconds",
                                        nameof(GetAppMetadataAsync),
                                        target.Id,
                                        _monitorConfiguration.DefaultTimeoutInSeconds);
                    });
                }

                using (CancellationTokenSource linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cancellationTokenSource.Token))
                {
                    (HttpResponseMessage response, string message) = await GetApplicationMetadataTask(target, linkedTokenSource.Token);

                    using (HttpResponseMessage httpResponseMessage = response)
                    {
                        IReadOnlyCollection <PackageVersion> packages =
                            await GetAllowedPackagesAsync(target, linkedTokenSource.Token);

                        if (httpResponseMessage == null)
                        {
                            return(new AppVersion(target,
                                                  message ?? $"Could not get application metadata from target {target.Url}, no response",
                                                  packages));
                        }

                        if (!httpResponseMessage.IsSuccessStatusCode)
                        {
                            return(new AppVersion(target,
                                                  message ??
                                                  $"Could not get application metadata from target {target.Url}, status code not successful {httpResponseMessage.StatusCode}",
                                                  packages));
                        }

                        appMetadata =
                            await GetAppVersionAsync(httpResponseMessage, target, packages, linkedTokenSource.Token);
                    }
                }
            }

            return(appMetadata);
        }
コード例 #17
0
        public void GetConfigFromTargetWithEmptyOldEnvironment()
        {
            var target = new DeploymentTarget(new DeploymentTargetId("123"), "123", "abc",
                                              environmentConfiguration: "");

            string?environmentConfig = target.GetEnvironmentConfiguration();

            Assert.True(string.IsNullOrWhiteSpace(environmentConfig));
        }
コード例 #18
0
 public AppVersion(
     [NotNull] DeploymentTarget target,
     [NotNull] IKeyValueConfiguration manifestProperties,
     IReadOnlyCollection <PackageVersion> availablePackageVersions)
 {
     Properties =
         manifestProperties ?? throw new ArgumentNullException(nameof(manifestProperties));
     AvailablePackageVersions = availablePackageVersions.SafeToImmutableArray();
     Target = target ?? throw new ArgumentNullException(nameof(target));
     Status = GetStatus();
 }
コード例 #19
0
 public AppVersion(
     [NotNull] DeploymentTarget target,
     string message,
     IReadOnlyCollection <PackageVersion> availablePackages)
 {
     Properties = new InMemoryKeyValueConfiguration(new NameValueCollection());
     Target     = target;
     Message    = message;
     AvailablePackageVersions = availablePackages.SafeToImmutableArray();
     Status = GetStatus();
 }
コード例 #20
0
        public void GetDeploymentPath_ShouldReturnCorrectDeploymentPath()
        {
            var deploymentTarget = new DeploymentTarget("Test");

            var expectedPath = Path.Combine("ExperimentName", "Test", $"ExperimentName.zip");

            // Act
            var deploymentPath = sut.GetDeploymentPath(deploymentTarget, "ExperimentName");

            // Assert
            deploymentPath.Should().Be(expectedPath);
        }
コード例 #21
0
        private static void CheckPackageMatchingTarget(DeploymentTarget deploymentTarget, string packageId)
        {
            if (
                !deploymentTarget.PackageId.Equals(packageId,
                                                   StringComparison.InvariantCultureIgnoreCase))
            {
                string allPackageIds = string.Join(", ",
                                                   deploymentTarget.PackageId.Select(name => $"'{name}'"));

                throw new DeployerAppException(
                          $"The package id '{packageId}' is not in the list of allowed package ids: {allPackageIds}");
            }
        }
コード例 #22
0
        public async Task <DeploymentTarget> GetDeploymentTargetAsync(
            string deploymentTargetId,
            CancellationToken cancellationToken = default)
        {
            ImmutableArray <OrganizationInfo> organizations = await GetOrganizationsAsync(cancellationToken);

            DeploymentTarget foundDeploymentTarget = organizations
                                                     .SelectMany(organizationInfo => organizationInfo.Projects)
                                                     .SelectMany(projectInfo => projectInfo.DeploymentTargets)
                                                     .SingleOrDefault(deploymentTarget => deploymentTarget.Id == deploymentTargetId);

            return(foundDeploymentTarget);
        }
コード例 #23
0
        public async Task <IActionResult> Edit(
            [FromRoute] string targetId,
            [FromServices] IDeploymentTargetReadService deploymentTargetReadService)
        {
            DeploymentTarget deploymentTarget = await deploymentTargetReadService.GetDeploymentTargetAsync(targetId);

            if (deploymentTarget is null)
            {
                return(RedirectToAction(nameof(Index)));
            }

            return(View(new EditTargetViewOutputModel(deploymentTarget)));
        }
コード例 #24
0
        public string GetDeploymentUri(Experiment experiment, DeploymentTarget deploymentTarget)
        {
            var deploymentPath = this.modelPathGenerator.GetDeploymentPath(deploymentTarget, experiment.ExperimentName);

            var request = new GetPreSignedUrlRequest
            {
                BucketName = deploymentRepositoryBucket,
                Key        = deploymentPath,
                Expires    = DateTime.Now.AddYears(5),
                Protocol   = Protocol.HTTP
            };

            return(this.s3Client.GetPreSignedURL(request));
        }
コード例 #25
0
        public async Task DeployModel_NoSourceFileExists_ShouldThrowException()
        {
            var experiment = new Experiment("ExperimentName");

            var registeredModel = new RegisteredModel
            {
                RunId = Guid.NewGuid(),
            };

            var deploymentTarget = new DeploymentTarget("Test");

            // Act
            await sut.DeployModelAsync(deploymentTarget, registeredModel, experiment);
        }
コード例 #26
0
        private async Task <string> DeployModelAsync(DeploymentTarget deploymentTarget)
        {
            var runId      = Guid.NewGuid();
            var experiment = new Experiment("ExperimentName");

            await sut.UploadModelAsync(runId, @"Data/model.txt");

            var registeredModel = new RegisteredModel
            {
                RunId        = runId,
                ExperimentId = experiment.ExperimentId
            };

            return(await sut.DeployModelAsync(deploymentTarget, registeredModel, experiment));
        }
コード例 #27
0
        public async Task DeployModel_NoSourceFileExist_ShouldThrowException()
        {
            var experiment = new Experiment("ExperimentName");

            var registeredModel = new RegisteredModel
            {
                RunId        = Guid.NewGuid(),
                ExperimentId = experiment.ExperimentId
            };

            var deploymentTarget = new DeploymentTarget("Test");

            var blobClientMock = new Mock <BlobClient>();
            var responseMock   = new Mock <Response <bool> >();

            blobClientMock.Setup(x => x.Exists(default)).Returns(responseMock.Object);
コード例 #28
0
        public async Task <string> DeployModelAsync(DeploymentTarget deploymentTarget, RegisteredModel registeredModel, Experiment experiment)
        {
            var deploymentPath = this.modelPathGenerator.GetDeploymentPath(deploymentTarget, experiment.ExperimentName);

            var sourceModelBlob   = this.modelRepositoryClient.GetBlobClient(this.modelPathGenerator.GetModelName(registeredModel.RunId));
            var deployedModelBlob = this.deploymentClient.GetBlobClient(deploymentPath);

            if (!sourceModelBlob.Exists())
            {
                throw new InvalidOperationException("The model to be deployed does not exist");
            }

            await deployedModelBlob.StartCopyFromUriAsync(sourceModelBlob.Uri);

            return(deployedModelBlob.Uri.ToString());
        }
コード例 #29
0
        private async Task UpdateTarget(CancellationToken cancellationToken,
                                        DeploymentTarget deploymentTarget,
                                        ImmutableArray <EnvironmentType> environmentTypes)
        {
            string?typeId = default;
            string?environmentConfiguration = deploymentTarget.EnvironmentConfiguration;

            if (string.IsNullOrWhiteSpace(deploymentTarget.EnvironmentTypeId) &&
                !string.IsNullOrWhiteSpace(environmentConfiguration))
            {
                string configuration = environmentConfiguration.Trim();

                EnvironmentType?foundType = environmentTypes.SingleOrDefault(type =>
                                                                             type.Name.Trim().Equals(configuration,
                                                                                                     StringComparison.OrdinalIgnoreCase));

                if (foundType is { })
コード例 #30
0
        ///<inheritdoc cref="IDeploymentRepository"/>
        public async Task <Deployment> CreateDeploymentAsync(DeploymentTarget deploymentTarget, RegisteredModel registeredModel, string deployedBy, string deploymentUri)
        {
            using var db = this.contextFactory.CreateDbContext();

            var deployment = new Deployment()
            {
                DeploymentDate     = this.clock.UtcNow,
                DeployedBy         = deployedBy,
                DeploymentTargetId = deploymentTarget.DeploymentTargetId,
                RegisteredModelId  = registeredModel.RegisteredModelId,
                DeploymentUri      = deploymentUri
            };

            db.Deployments.Add(deployment);
            await db.SaveChangesAsync();

            return(deployment);
        }
コード例 #31
0
        /// <summary>
        /// Configures the deployment 
        /// </summary>
        /// <param name="deployment"></param>
        /// <param name="portalInstanceId"></param>
        private void InitializeDeployment(Sage.Platform.Deployment.Deployment deployment, string portalInstanceId)
        {
            // Configure the portal
            var portal = new DeploymentTargetPortal
            {
                PortalName = Manifest.PortalName,
                IsActive = true,
                InstanceId = portalInstanceId
            };

            // Configure the target
            var target = new DeploymentTarget("FS")
            {
                IsActive = true
            };

            target.ExtendedProperties.SetValue(DeploymentTargetConstants.DeploymentPath, Manifest.DeploymentPath);
            target.Portals.Add(portal);
            deployment.Targets.Add(target);
        }
コード例 #32
0
 public TargetPortalInfo(DeploymentTarget target, DeploymentTargetPortal targetPortal)
 {
     Target = target;
     TargetPortal = targetPortal;
 }