mirror of
https://github.com/digital-asset/daml.git
synced 2024-08-16 16:20:48 +03:00
do a proper canton 3.x code drop in the canton-3x directory (#17980)
* do a proper canton 3.x code drop in the canton-3x directory * copy the code from canton3 * address Gary's comments * fix canton-3x
This commit is contained in:
parent
aa67b0bf30
commit
56018b5d6e
@ -7,4 +7,4 @@ language-support/ts/node_modules/
|
||||
language-support/ts/packages/node_modules/
|
||||
navigator/frontend/node_modules/
|
||||
compatibility/
|
||||
canton-3x/
|
||||
|
||||
|
@ -223,7 +223,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
git fetch
|
||||
git checkout origin/main
|
||||
ci/build-canton-3x.sh HEAD
|
||||
ci/build-canton-3x.sh
|
||||
env:
|
||||
GITHUB_TOKEN: $(CANTON_READONLY_TOKEN)
|
||||
- template: ci/tell-slack-failed.yml
|
||||
|
201
canton-3x/community/LICENSE-open-source-bundle.txt
Normal file
201
canton-3x/community/LICENSE-open-source-bundle.txt
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client
|
||||
|
||||
import cats.data.EitherT
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand
|
||||
import com.digitalasset.canton.ledger.api.auth.client.LedgerCallCredentials
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.networking.grpc.CantonGrpcUtil
|
||||
import com.digitalasset.canton.tracing.{TraceContext, TraceContextGrpc}
|
||||
import com.digitalasset.canton.util.LoggerUtil
|
||||
import io.grpc.ManagedChannel
|
||||
import io.grpc.stub.AbstractStub
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
/** Run a command using the default workflow
|
||||
*/
|
||||
class GrpcCtlRunner(
|
||||
maxRequestDebugLines: Int,
|
||||
maxRequestDebugStringLength: Int,
|
||||
val loggerFactory: NamedLoggerFactory,
|
||||
) extends NamedLogging {
|
||||
|
||||
/** Runs a command
|
||||
* @return Either a printable error as a String or a Unit indicating all was successful
|
||||
*/
|
||||
def run[Req, Res, Result](
|
||||
instanceName: String,
|
||||
command: GrpcAdminCommand[Req, Res, Result],
|
||||
channel: ManagedChannel,
|
||||
token: Option[String],
|
||||
timeout: Duration,
|
||||
)(implicit ec: ExecutionContext, traceContext: TraceContext): EitherT[Future, String, Result] = {
|
||||
|
||||
val baseService: command.Svc = command
|
||||
.createService(channel)
|
||||
.withInterceptors(TraceContextGrpc.clientInterceptor)
|
||||
|
||||
val service = token.fold(baseService)(LedgerCallCredentials.authenticatingStub(baseService, _))
|
||||
|
||||
for {
|
||||
request <- EitherT.fromEither[Future](command.createRequest())
|
||||
response <- submitRequest(command)(instanceName, service, request, timeout)
|
||||
result <- EitherT.fromEither[Future](command.handleResponse(response))
|
||||
} yield result
|
||||
}
|
||||
|
||||
private def submitRequest[Svc <: AbstractStub[Svc], Req, Res, Result](
|
||||
command: GrpcAdminCommand[Req, Res, Result]
|
||||
)(instanceName: String, service: command.Svc, request: Req, timeout: Duration)(implicit
|
||||
ec: ExecutionContext,
|
||||
traceContext: TraceContext,
|
||||
): EitherT[Future, String, Res] =
|
||||
CantonGrpcUtil
|
||||
.sendGrpcRequest(service, instanceName)(
|
||||
command.submitRequest(_, request),
|
||||
LoggerUtil.truncateString(maxRequestDebugLines, maxRequestDebugStringLength)(
|
||||
command.toString
|
||||
),
|
||||
timeout,
|
||||
logger,
|
||||
CantonGrpcUtil.silentLogPolicy, // silent log policy, as the ConsoleEnvironment will log the result
|
||||
_ => false, // no retry to optimize for low latency
|
||||
)
|
||||
.leftMap(_.toString)
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import com.digitalasset.canton.DiscardOps
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
DefaultBoundedTimeout,
|
||||
TimeoutType,
|
||||
}
|
||||
import com.digitalasset.canton.config.NonNegativeDuration
|
||||
import io.grpc.stub.{AbstractStub, StreamObserver}
|
||||
import io.grpc.{Context, ManagedChannel, Status, StatusException, StatusRuntimeException}
|
||||
|
||||
import java.util.concurrent.{ScheduledExecutorService, TimeUnit}
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.{Future, Promise, blocking}
|
||||
|
||||
trait AdminCommand[Req, Res, Result] {
|
||||
|
||||
/** Create the request from configured options
|
||||
*/
|
||||
def createRequest(): Either[String, Req]
|
||||
|
||||
/** Handle the response the service has provided
|
||||
*/
|
||||
def handleResponse(response: Res): Either[String, Result]
|
||||
|
||||
/** Determines within which time frame the request should complete
|
||||
*
|
||||
* Some requests can run for a very long time. In this case, they should be "unbounded".
|
||||
* For other requests, you will want to set a custom timeout apart from the global default bounded timeout
|
||||
*/
|
||||
def timeoutType: TimeoutType = DefaultBoundedTimeout
|
||||
|
||||
/** Command's full name used to identify command in logging and span reporting
|
||||
*/
|
||||
def fullName: String =
|
||||
// not using getClass.getSimpleName because it ignores the hierarchy of nested classes, and it also throws unexpected exceptions
|
||||
getClass.getName.split('.').last.replace("$", ".")
|
||||
}
|
||||
|
||||
/** cantonctl GRPC Command
|
||||
*/
|
||||
trait GrpcAdminCommand[Req, Res, Result] extends AdminCommand[Req, Res, Result] {
|
||||
|
||||
type Svc <: AbstractStub[Svc]
|
||||
|
||||
/** Create the GRPC service to call
|
||||
*/
|
||||
def createService(channel: ManagedChannel): Svc
|
||||
|
||||
/** Submit the created request to our service
|
||||
*/
|
||||
def submitRequest(service: Svc, request: Req): Future[Res]
|
||||
|
||||
}
|
||||
|
||||
object GrpcAdminCommand {
|
||||
sealed trait TimeoutType extends Product with Serializable
|
||||
|
||||
/** Custom timeout triggered by the client */
|
||||
final case class CustomClientTimeout(timeout: NonNegativeDuration) extends TimeoutType
|
||||
|
||||
/** The Server will ensure the operation is timed out so the client timeout is set to an infinite value */
|
||||
case object ServerEnforcedTimeout extends TimeoutType
|
||||
case object DefaultBoundedTimeout extends TimeoutType
|
||||
case object DefaultUnboundedTimeout extends TimeoutType
|
||||
|
||||
object GrpcErrorStatus {
|
||||
def unapply(ex: Throwable): Option[Status] = ex match {
|
||||
case e: StatusException => Some(e.getStatus)
|
||||
case re: StatusRuntimeException => Some(re.getStatus)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
private[digitalasset] def streamedResponse[Request, Response, Result](
|
||||
service: (Request, StreamObserver[Response]) => Unit,
|
||||
extract: Response => Seq[Result],
|
||||
request: Request,
|
||||
expected: Int,
|
||||
timeout: FiniteDuration,
|
||||
scheduler: ScheduledExecutorService,
|
||||
): Future[Seq[Result]] = {
|
||||
val promise = Promise[Seq[Result]]()
|
||||
val buffer = ListBuffer[Result]()
|
||||
val context = Context.ROOT.withCancellation()
|
||||
|
||||
def success(): Unit = blocking(buffer.synchronized {
|
||||
context.close()
|
||||
promise.trySuccess(buffer.toList).discard[Boolean]
|
||||
})
|
||||
|
||||
context.run(() =>
|
||||
service(
|
||||
request,
|
||||
new StreamObserver[Response]() {
|
||||
override def onNext(value: Response): Unit = {
|
||||
val extracted = extract(value)
|
||||
blocking(buffer.synchronized {
|
||||
if (buffer.lengthCompare(expected) < 0) {
|
||||
buffer ++= extracted
|
||||
if (buffer.lengthCompare(expected) >= 0) {
|
||||
success()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override def onError(t: Throwable): Unit = {
|
||||
t match {
|
||||
case GrpcErrorStatus(status) if status.getCode == Status.CANCELLED.getCode =>
|
||||
success()
|
||||
case _ =>
|
||||
val _ = promise.tryFailure(t)
|
||||
}
|
||||
}
|
||||
|
||||
override def onCompleted(): Unit = {
|
||||
success()
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
scheduler.schedule(
|
||||
new Runnable() {
|
||||
override def run(): Unit = {
|
||||
val _ = context.cancel(Status.CANCELLED.asException())
|
||||
}
|
||||
},
|
||||
timeout.toMillis,
|
||||
TimeUnit.MILLISECONDS,
|
||||
)
|
||||
promise.future
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.digitalasset.canton.admin.api.client.data.StaticDomainParameters as StaticDomainParametersConfig
|
||||
import com.digitalasset.canton.domain.admin.v0 as adminproto
|
||||
import com.digitalasset.canton.domain.service.ServiceAgreementAcceptance
|
||||
import com.digitalasset.canton.protocol.StaticDomainParameters as StaticDomainParametersInternal
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ManagedChannel
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object DomainAdminCommands {
|
||||
|
||||
abstract class BaseDomainServiceCommand[Req, Rep, Res] extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc = adminproto.DomainServiceGrpc.DomainServiceStub
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): adminproto.DomainServiceGrpc.DomainServiceStub =
|
||||
adminproto.DomainServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case object ListAcceptedServiceAgreements
|
||||
extends BaseDomainServiceCommand[Empty, adminproto.ServiceAgreementAcceptances, Seq[
|
||||
ServiceAgreementAcceptance
|
||||
]] {
|
||||
override def createRequest(): Either[String, Empty] = Right(Empty())
|
||||
|
||||
override def submitRequest(
|
||||
service: adminproto.DomainServiceGrpc.DomainServiceStub,
|
||||
request: Empty,
|
||||
): Future[adminproto.ServiceAgreementAcceptances] =
|
||||
service.listServiceAgreementAcceptances(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: adminproto.ServiceAgreementAcceptances
|
||||
): Either[String, Seq[ServiceAgreementAcceptance]] =
|
||||
response.acceptances
|
||||
.traverse(ServiceAgreementAcceptance.fromProtoV0)
|
||||
.bimap(_.toString, _.toSeq)
|
||||
}
|
||||
|
||||
final case class GetDomainParameters()
|
||||
extends BaseDomainServiceCommand[
|
||||
adminproto.GetDomainParameters.Request,
|
||||
adminproto.GetDomainParameters.Response,
|
||||
StaticDomainParametersConfig,
|
||||
] {
|
||||
override def createRequest(): Either[String, adminproto.GetDomainParameters.Request] = Right(
|
||||
adminproto.GetDomainParameters.Request()
|
||||
)
|
||||
override def submitRequest(
|
||||
service: adminproto.DomainServiceGrpc.DomainServiceStub,
|
||||
request: adminproto.GetDomainParameters.Request,
|
||||
): Future[adminproto.GetDomainParameters.Response] =
|
||||
service.getDomainParametersVersioned(adminproto.GetDomainParameters.Request())
|
||||
|
||||
override def handleResponse(
|
||||
response: adminproto.GetDomainParameters.Response
|
||||
): Either[String, StaticDomainParametersConfig] = {
|
||||
import adminproto.GetDomainParameters.Response.Parameters
|
||||
|
||||
response.parameters match {
|
||||
case Parameters.Empty => Left("Field parameters was not found in the response")
|
||||
case Parameters.ParametersV1(parametersV1) =>
|
||||
(for {
|
||||
staticDomainParametersInternal <- StaticDomainParametersInternal.fromProtoV1(
|
||||
parametersV1
|
||||
)
|
||||
staticDomainParametersConfig = StaticDomainParametersConfig(
|
||||
staticDomainParametersInternal
|
||||
)
|
||||
} yield staticDomainParametersConfig).leftMap(_.toString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
CustomClientTimeout,
|
||||
TimeoutType,
|
||||
}
|
||||
import com.digitalasset.canton.config.NonNegativeDuration
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.domain.api.v0
|
||||
import com.digitalasset.canton.domain.api.v0.DomainTimeServiceGrpc.DomainTimeServiceStub
|
||||
import com.digitalasset.canton.time.{
|
||||
AwaitTimeRequest,
|
||||
FetchTimeRequest,
|
||||
FetchTimeResponse,
|
||||
NonNegativeFiniteDuration,
|
||||
}
|
||||
import com.digitalasset.canton.topology.DomainId
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ManagedChannel
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object DomainTimeCommands {
|
||||
|
||||
abstract class BaseDomainTimeCommand[Req, Rep, Res] extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc = DomainTimeServiceStub
|
||||
override def createService(channel: ManagedChannel): DomainTimeServiceStub =
|
||||
v0.DomainTimeServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case class FetchTime(
|
||||
domainIdO: Option[DomainId],
|
||||
freshnessBound: NonNegativeFiniteDuration,
|
||||
timeout: NonNegativeDuration,
|
||||
) extends BaseDomainTimeCommand[FetchTimeRequest, v0.FetchTimeResponse, FetchTimeResponse] {
|
||||
|
||||
override def createRequest(): Either[String, FetchTimeRequest] =
|
||||
Right(FetchTimeRequest(domainIdO, freshnessBound))
|
||||
|
||||
override def submitRequest(
|
||||
service: DomainTimeServiceStub,
|
||||
request: FetchTimeRequest,
|
||||
): Future[v0.FetchTimeResponse] =
|
||||
service.fetchTime(request.toProtoV0)
|
||||
|
||||
override def handleResponse(response: v0.FetchTimeResponse): Either[String, FetchTimeResponse] =
|
||||
FetchTimeResponse.fromProto(response).leftMap(_.toString)
|
||||
|
||||
override def timeoutType: TimeoutType = CustomClientTimeout(timeout)
|
||||
}
|
||||
|
||||
final case class AwaitTime(
|
||||
domainIdO: Option[DomainId],
|
||||
time: CantonTimestamp,
|
||||
timeout: NonNegativeDuration,
|
||||
) extends BaseDomainTimeCommand[AwaitTimeRequest, Empty, Unit] {
|
||||
|
||||
override def createRequest(): Either[String, AwaitTimeRequest] =
|
||||
Right(AwaitTimeRequest(domainIdO, time))
|
||||
|
||||
override def submitRequest(
|
||||
service: DomainTimeServiceStub,
|
||||
request: AwaitTimeRequest,
|
||||
): Future[Empty] =
|
||||
service.awaitTime(request.toProtoV0)
|
||||
|
||||
override def handleResponse(response: Empty): Either[String, Unit] = Right(())
|
||||
|
||||
override def timeoutType: TimeoutType = CustomClientTimeout(timeout)
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.option.*
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
DefaultUnboundedTimeout,
|
||||
TimeoutType,
|
||||
}
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.crypto.{Fingerprint, PublicKey}
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.domain.admin.v0.EnterpriseMediatorAdministrationServiceGrpc
|
||||
import com.digitalasset.canton.domain.admin.{v0, v2}
|
||||
import com.digitalasset.canton.domain.mediator.admin.gprc.{
|
||||
InitializeMediatorRequest,
|
||||
InitializeMediatorRequestX,
|
||||
InitializeMediatorResponse,
|
||||
InitializeMediatorResponseX,
|
||||
}
|
||||
import com.digitalasset.canton.protocol.StaticDomainParameters
|
||||
import com.digitalasset.canton.pruning.admin.v0.LocatePruningTimestamp
|
||||
import com.digitalasset.canton.sequencing.SequencerConnections
|
||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactions
|
||||
import com.digitalasset.canton.topology.transaction.TopologyChangeOp
|
||||
import com.digitalasset.canton.topology.{DomainId, MediatorId}
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ManagedChannel
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object EnterpriseMediatorAdministrationCommands {
|
||||
abstract class BaseMediatorInitializationCommand[Req, Rep, Res]
|
||||
extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc = v0.MediatorInitializationServiceGrpc.MediatorInitializationServiceStub
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): v0.MediatorInitializationServiceGrpc.MediatorInitializationServiceStub =
|
||||
v0.MediatorInitializationServiceGrpc.stub(channel)
|
||||
}
|
||||
abstract class BaseMediatorXInitializationCommand[Req, Rep, Res]
|
||||
extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc = v2.MediatorInitializationServiceGrpc.MediatorInitializationServiceStub
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): v2.MediatorInitializationServiceGrpc.MediatorInitializationServiceStub =
|
||||
v2.MediatorInitializationServiceGrpc.stub(channel)
|
||||
}
|
||||
abstract class BaseMediatorAdministrationCommand[Req, Rep, Res]
|
||||
extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc =
|
||||
v0.EnterpriseMediatorAdministrationServiceGrpc.EnterpriseMediatorAdministrationServiceStub
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): v0.EnterpriseMediatorAdministrationServiceGrpc.EnterpriseMediatorAdministrationServiceStub =
|
||||
v0.EnterpriseMediatorAdministrationServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case class Initialize(
|
||||
domainId: DomainId,
|
||||
mediatorId: MediatorId,
|
||||
topologyState: Option[StoredTopologyTransactions[TopologyChangeOp.Positive]],
|
||||
domainParameters: StaticDomainParameters,
|
||||
sequencerConnections: SequencerConnections,
|
||||
signingKeyFingerprint: Option[Fingerprint],
|
||||
) extends BaseMediatorInitializationCommand[
|
||||
v0.InitializeMediatorRequest,
|
||||
v0.InitializeMediatorResponse,
|
||||
PublicKey,
|
||||
] {
|
||||
override def createRequest(): Either[String, v0.InitializeMediatorRequest] =
|
||||
Right(
|
||||
InitializeMediatorRequest(
|
||||
domainId,
|
||||
mediatorId,
|
||||
topologyState,
|
||||
domainParameters,
|
||||
sequencerConnections,
|
||||
signingKeyFingerprint,
|
||||
).toProtoV0
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: v0.MediatorInitializationServiceGrpc.MediatorInitializationServiceStub,
|
||||
request: v0.InitializeMediatorRequest,
|
||||
): Future[v0.InitializeMediatorResponse] =
|
||||
service.initialize(request)
|
||||
override def handleResponse(
|
||||
response: v0.InitializeMediatorResponse
|
||||
): Either[String, PublicKey] =
|
||||
InitializeMediatorResponse
|
||||
.fromProtoV0(response)
|
||||
.leftMap(err => s"Failed to deserialize response: $err")
|
||||
.flatMap(_.toEither)
|
||||
}
|
||||
|
||||
final case class InitializeX(
|
||||
domainId: DomainId,
|
||||
domainParameters: StaticDomainParameters,
|
||||
sequencerConnections: SequencerConnections,
|
||||
) extends BaseMediatorXInitializationCommand[
|
||||
v2.InitializeMediatorRequest,
|
||||
v2.InitializeMediatorResponse,
|
||||
Unit,
|
||||
] {
|
||||
override def createRequest(): Either[String, v2.InitializeMediatorRequest] =
|
||||
Right(
|
||||
InitializeMediatorRequestX(
|
||||
domainId,
|
||||
domainParameters,
|
||||
sequencerConnections,
|
||||
).toProtoV2
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: v2.MediatorInitializationServiceGrpc.MediatorInitializationServiceStub,
|
||||
request: v2.InitializeMediatorRequest,
|
||||
): Future[v2.InitializeMediatorResponse] =
|
||||
service.initialize(request)
|
||||
override def handleResponse(
|
||||
response: v2.InitializeMediatorResponse
|
||||
): Either[String, Unit] =
|
||||
InitializeMediatorResponseX
|
||||
.fromProtoV2(response)
|
||||
.leftMap(err => s"Failed to deserialize response: $err")
|
||||
.map(_ => ())
|
||||
|
||||
}
|
||||
|
||||
final case class Prune(timestamp: CantonTimestamp)
|
||||
extends GrpcAdminCommand[v0.MediatorPruningRequest, Empty, Unit] {
|
||||
override type Svc =
|
||||
v0.EnterpriseMediatorAdministrationServiceGrpc.EnterpriseMediatorAdministrationServiceStub
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): v0.EnterpriseMediatorAdministrationServiceGrpc.EnterpriseMediatorAdministrationServiceStub =
|
||||
v0.EnterpriseMediatorAdministrationServiceGrpc.stub(channel)
|
||||
override def createRequest(): Either[String, v0.MediatorPruningRequest] =
|
||||
Right(v0.MediatorPruningRequest(timestamp.toProtoPrimitive.some))
|
||||
override def submitRequest(
|
||||
service: v0.EnterpriseMediatorAdministrationServiceGrpc.EnterpriseMediatorAdministrationServiceStub,
|
||||
request: v0.MediatorPruningRequest,
|
||||
): Future[Empty] = service.prune(request)
|
||||
override def handleResponse(response: Empty): Either[String, Unit] = Right(())
|
||||
|
||||
// all pruning commands will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
}
|
||||
|
||||
final case class LocatePruningTimestampCommand(index: PositiveInt)
|
||||
extends BaseMediatorAdministrationCommand[
|
||||
LocatePruningTimestamp.Request,
|
||||
LocatePruningTimestamp.Response,
|
||||
Option[CantonTimestamp],
|
||||
] {
|
||||
override def createRequest(): Either[String, LocatePruningTimestamp.Request] = Right(
|
||||
LocatePruningTimestamp.Request(index.value)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: EnterpriseMediatorAdministrationServiceGrpc.EnterpriseMediatorAdministrationServiceStub,
|
||||
request: LocatePruningTimestamp.Request,
|
||||
): Future[LocatePruningTimestamp.Response] =
|
||||
service.locatePruningTimestamp(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: LocatePruningTimestamp.Response
|
||||
): Either[String, Option[CantonTimestamp]] =
|
||||
response.timestamp.fold(Right(None): Either[String, Option[CantonTimestamp]])(
|
||||
CantonTimestamp.fromProtoPrimitive(_).bimap(_.message, Some(_))
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,289 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.option.*
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
DefaultUnboundedTimeout,
|
||||
TimeoutType,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.data.StaticDomainParameters
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.domain.admin.v2.SequencerInitializationServiceGrpc
|
||||
import com.digitalasset.canton.domain.admin.{v0, v2}
|
||||
import com.digitalasset.canton.domain.sequencing.admin.grpc.{
|
||||
InitializeSequencerRequest,
|
||||
InitializeSequencerRequestX,
|
||||
InitializeSequencerResponse,
|
||||
InitializeSequencerResponseX,
|
||||
}
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.{LedgerIdentity, SequencerSnapshot}
|
||||
import com.digitalasset.canton.pruning.admin.v0.LocatePruningTimestamp
|
||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactions
|
||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.GenericStoredTopologyTransactionsX
|
||||
import com.digitalasset.canton.topology.transaction.TopologyChangeOp
|
||||
import com.digitalasset.canton.topology.{DomainId, Member}
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ManagedChannel
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object EnterpriseSequencerAdminCommands {
|
||||
abstract class BaseSequencerInitializationCommand[Req, Rep, Res]
|
||||
extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc = v0.SequencerInitializationServiceGrpc.SequencerInitializationServiceStub
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): v0.SequencerInitializationServiceGrpc.SequencerInitializationServiceStub =
|
||||
v0.SequencerInitializationServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
abstract class BaseSequencerAdministrationCommand[Req, Rep, Res]
|
||||
extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc =
|
||||
v0.EnterpriseSequencerAdministrationServiceGrpc.EnterpriseSequencerAdministrationServiceStub
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): v0.EnterpriseSequencerAdministrationServiceGrpc.EnterpriseSequencerAdministrationServiceStub =
|
||||
v0.EnterpriseSequencerAdministrationServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
abstract class BaseSequencerTopologyBootstrapCommand[Req, Rep, Res]
|
||||
extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc = v0.TopologyBootstrapServiceGrpc.TopologyBootstrapServiceStub
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): v0.TopologyBootstrapServiceGrpc.TopologyBootstrapServiceStub =
|
||||
v0.TopologyBootstrapServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
sealed trait Initialize[ProtoRequest]
|
||||
extends BaseSequencerInitializationCommand[
|
||||
ProtoRequest,
|
||||
v0.InitResponse,
|
||||
InitializeSequencerResponse,
|
||||
] {
|
||||
protected def domainId: DomainId
|
||||
protected def topologySnapshot: StoredTopologyTransactions[TopologyChangeOp.Positive]
|
||||
|
||||
protected def domainParameters: StaticDomainParameters
|
||||
|
||||
protected def snapshotO: Option[SequencerSnapshot]
|
||||
|
||||
protected def serializer: InitializeSequencerRequest => ProtoRequest
|
||||
|
||||
override def createRequest(): Either[String, ProtoRequest] = {
|
||||
val request = InitializeSequencerRequest(
|
||||
domainId,
|
||||
topologySnapshot,
|
||||
domainParameters.toInternal,
|
||||
snapshotO,
|
||||
)
|
||||
Right(serializer(request))
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.InitResponse
|
||||
): Either[String, InitializeSequencerResponse] =
|
||||
InitializeSequencerResponse
|
||||
.fromProtoV0(response)
|
||||
.leftMap(err => s"Failed to deserialize response: $err")
|
||||
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
}
|
||||
|
||||
object Initialize {
|
||||
final case class V2(
|
||||
domainId: DomainId,
|
||||
topologySnapshot: StoredTopologyTransactions[TopologyChangeOp.Positive],
|
||||
domainParameters: StaticDomainParameters,
|
||||
snapshotO: Option[SequencerSnapshot],
|
||||
) extends Initialize[v2.InitRequest] {
|
||||
|
||||
override protected def serializer: InitializeSequencerRequest => v2.InitRequest = _.toProtoV2
|
||||
|
||||
override def submitRequest(
|
||||
service: v0.SequencerInitializationServiceGrpc.SequencerInitializationServiceStub,
|
||||
request: v2.InitRequest,
|
||||
): Future[v0.InitResponse] =
|
||||
service.initV2(request)
|
||||
}
|
||||
|
||||
def apply(
|
||||
domainId: DomainId,
|
||||
topologySnapshot: StoredTopologyTransactions[TopologyChangeOp.Positive],
|
||||
domainParameters: StaticDomainParameters,
|
||||
snapshotO: Option[SequencerSnapshot] = None,
|
||||
): Initialize[_] =
|
||||
V2(domainId, topologySnapshot, domainParameters, snapshotO)
|
||||
}
|
||||
|
||||
final case class InitializeX(
|
||||
topologySnapshot: GenericStoredTopologyTransactionsX,
|
||||
domainParameters: com.digitalasset.canton.protocol.StaticDomainParameters,
|
||||
sequencerSnapshot: Option[SequencerSnapshot],
|
||||
) extends GrpcAdminCommand[
|
||||
v2.InitializeSequencerRequest,
|
||||
v2.InitializeSequencerResponse,
|
||||
InitializeSequencerResponseX,
|
||||
] {
|
||||
override type Svc = v2.SequencerInitializationServiceGrpc.SequencerInitializationServiceStub
|
||||
|
||||
override def createService(
|
||||
channel: ManagedChannel
|
||||
): SequencerInitializationServiceGrpc.SequencerInitializationServiceStub =
|
||||
v2.SequencerInitializationServiceGrpc.stub(channel)
|
||||
|
||||
override def submitRequest(
|
||||
service: SequencerInitializationServiceGrpc.SequencerInitializationServiceStub,
|
||||
request: v2.InitializeSequencerRequest,
|
||||
): Future[v2.InitializeSequencerResponse] =
|
||||
service.initialize(request)
|
||||
|
||||
override def createRequest(): Either[String, v2.InitializeSequencerRequest] =
|
||||
Right(
|
||||
InitializeSequencerRequestX(
|
||||
topologySnapshot,
|
||||
domainParameters,
|
||||
sequencerSnapshot,
|
||||
).toProtoV2
|
||||
)
|
||||
|
||||
override def handleResponse(
|
||||
response: v2.InitializeSequencerResponse
|
||||
): Either[String, InitializeSequencerResponseX] =
|
||||
InitializeSequencerResponseX.fromProtoV2(response).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class Snapshot(timestamp: CantonTimestamp)
|
||||
extends BaseSequencerAdministrationCommand[
|
||||
v0.Snapshot.Request,
|
||||
v0.Snapshot.Response,
|
||||
SequencerSnapshot,
|
||||
] {
|
||||
override def createRequest(): Either[String, v0.Snapshot.Request] = {
|
||||
Right(v0.Snapshot.Request(Some(timestamp.toProtoPrimitive)))
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: v0.EnterpriseSequencerAdministrationServiceGrpc.EnterpriseSequencerAdministrationServiceStub,
|
||||
request: v0.Snapshot.Request,
|
||||
): Future[v0.Snapshot.Response] = service.snapshot(request)
|
||||
|
||||
override def handleResponse(response: v0.Snapshot.Response): Either[String, SequencerSnapshot] =
|
||||
response.value match {
|
||||
case v0.Snapshot.Response.Value.Failure(v0.Snapshot.Failure(reason)) => Left(reason)
|
||||
case v0.Snapshot.Response.Value.Success(v0.Snapshot.Success(Some(result))) =>
|
||||
SequencerSnapshot.fromProtoV1(result).leftMap(_.toString)
|
||||
case v0.Snapshot.Response.Value.VersionedSuccess(v0.Snapshot.VersionedSuccess(snapshot)) =>
|
||||
SequencerSnapshot.fromByteString(snapshot).leftMap(_.toString)
|
||||
case _ => Left("response is empty")
|
||||
}
|
||||
|
||||
// command will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
final case class Prune(timestamp: CantonTimestamp)
|
||||
extends BaseSequencerAdministrationCommand[v0.Pruning.Request, v0.Pruning.Response, String] {
|
||||
override def createRequest(): Either[String, v0.Pruning.Request] =
|
||||
Right(v0.Pruning.Request(timestamp.toProtoPrimitive.some))
|
||||
|
||||
override def submitRequest(
|
||||
service: v0.EnterpriseSequencerAdministrationServiceGrpc.EnterpriseSequencerAdministrationServiceStub,
|
||||
request: v0.Pruning.Request,
|
||||
): Future[v0.Pruning.Response] =
|
||||
service.prune(request)
|
||||
override def handleResponse(response: v0.Pruning.Response): Either[String, String] =
|
||||
Either.cond(
|
||||
response.details.nonEmpty,
|
||||
response.details,
|
||||
"Pruning response did not contain details",
|
||||
)
|
||||
|
||||
// command will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
final case class LocatePruningTimestampCommand(index: PositiveInt)
|
||||
extends BaseSequencerAdministrationCommand[
|
||||
LocatePruningTimestamp.Request,
|
||||
LocatePruningTimestamp.Response,
|
||||
Option[CantonTimestamp],
|
||||
] {
|
||||
override def createRequest(): Either[String, LocatePruningTimestamp.Request] = Right(
|
||||
LocatePruningTimestamp.Request(index.value)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: v0.EnterpriseSequencerAdministrationServiceGrpc.EnterpriseSequencerAdministrationServiceStub,
|
||||
request: LocatePruningTimestamp.Request,
|
||||
): Future[LocatePruningTimestamp.Response] =
|
||||
service.locatePruningTimestamp(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: LocatePruningTimestamp.Response
|
||||
): Either[String, Option[CantonTimestamp]] =
|
||||
response.timestamp.fold(Right(None): Either[String, Option[CantonTimestamp]])(
|
||||
CantonTimestamp.fromProtoPrimitive(_).bimap(_.message, Some(_))
|
||||
)
|
||||
}
|
||||
|
||||
final case class DisableMember(member: Member)
|
||||
extends BaseSequencerAdministrationCommand[v0.DisableMemberRequest, Empty, Unit] {
|
||||
override def createRequest(): Either[String, v0.DisableMemberRequest] =
|
||||
Right(v0.DisableMemberRequest(member.toProtoPrimitive))
|
||||
override def submitRequest(
|
||||
service: v0.EnterpriseSequencerAdministrationServiceGrpc.EnterpriseSequencerAdministrationServiceStub,
|
||||
request: v0.DisableMemberRequest,
|
||||
): Future[Empty] = service.disableMember(request)
|
||||
override def handleResponse(response: Empty): Either[String, Unit] = Right(())
|
||||
}
|
||||
|
||||
final case class AuthorizeLedgerIdentity(ledgerIdentity: LedgerIdentity)
|
||||
extends BaseSequencerAdministrationCommand[
|
||||
v0.LedgerIdentity.AuthorizeRequest,
|
||||
v0.LedgerIdentity.AuthorizeResponse,
|
||||
Unit,
|
||||
] {
|
||||
override def createRequest(): Either[String, v0.LedgerIdentity.AuthorizeRequest] =
|
||||
Right(v0.LedgerIdentity.AuthorizeRequest(Some(ledgerIdentity.toProtoV0)))
|
||||
override def submitRequest(
|
||||
service: v0.EnterpriseSequencerAdministrationServiceGrpc.EnterpriseSequencerAdministrationServiceStub,
|
||||
request: v0.LedgerIdentity.AuthorizeRequest,
|
||||
): Future[v0.LedgerIdentity.AuthorizeResponse] = service.authorizeLedgerIdentity(request)
|
||||
override def handleResponse(
|
||||
response: v0.LedgerIdentity.AuthorizeResponse
|
||||
): Either[String, Unit] = response.value match {
|
||||
case v0.LedgerIdentity.AuthorizeResponse.Value.Failure(v0.LedgerIdentity.Failure(reason)) =>
|
||||
Left(reason)
|
||||
case v0.LedgerIdentity.AuthorizeResponse.Value.Success(v0.LedgerIdentity.Success()) =>
|
||||
Right(())
|
||||
case other => Left(s"Empty response: $other")
|
||||
}
|
||||
}
|
||||
|
||||
final case class BootstrapTopology(
|
||||
topologySnapshot: StoredTopologyTransactions[TopologyChangeOp.Positive]
|
||||
) extends BaseSequencerTopologyBootstrapCommand[v0.TopologyBootstrapRequest, Empty, Unit] {
|
||||
override def createRequest(): Either[String, v0.TopologyBootstrapRequest] =
|
||||
Right(v0.TopologyBootstrapRequest(Some(topologySnapshot.toProtoV0)))
|
||||
|
||||
override def submitRequest(
|
||||
service: v0.TopologyBootstrapServiceGrpc.TopologyBootstrapServiceStub,
|
||||
request: v0.TopologyBootstrapRequest,
|
||||
): Future[Empty] =
|
||||
service.bootstrap(request)
|
||||
|
||||
override def handleResponse(response: Empty): Either[String, Unit] = Right(())
|
||||
|
||||
// command will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import com.daml.ledger.api.v1.event.CreatedEvent
|
||||
import com.daml.ledger.api.v1.value.{Record, RecordField, Value}
|
||||
import com.daml.lf.data.Time
|
||||
import com.daml.lf.transaction.TransactionCoder
|
||||
import com.digitalasset.canton.admin.api.client.data.TemplateId
|
||||
import com.digitalasset.canton.crypto.Salt
|
||||
import com.digitalasset.canton.ledger.api.util.TimestampConversion
|
||||
import com.digitalasset.canton.protocol.{DriverContractMetadata, LfContractId}
|
||||
|
||||
/** Wrapper class to make scalapb LedgerApi classes more convenient to access
|
||||
*/
|
||||
object LedgerApiTypeWrappers {
|
||||
|
||||
/*
|
||||
Provide a few utilities methods on CreatedEvent.
|
||||
Notes:
|
||||
* We don't use an `implicit class` because it makes the use of pretty
|
||||
instances difficult (e.g. for `ledger_api.acs.of_all`).
|
||||
|
||||
* Also, the name of some methods of `WrappedCreatedEvent`, such as `templateId`,
|
||||
collides with one of the underlying event.
|
||||
*/
|
||||
final case class WrappedCreatedEvent(event: CreatedEvent) {
|
||||
|
||||
private def corrupt: String = s"corrupt event ${event.eventId} / ${event.contractId}"
|
||||
|
||||
def templateId: TemplateId = {
|
||||
TemplateId.fromIdentifier(
|
||||
event.templateId.getOrElse(
|
||||
throw new IllegalArgumentException(
|
||||
s"Template Id not specified for event ${event.eventId} / ${event.contractId}"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
def packageId: String = {
|
||||
event.templateId.map(_.packageId).getOrElse(corrupt)
|
||||
}
|
||||
|
||||
private def flatten(prefix: Seq[String], field: RecordField): Seq[(String, Any)] = {
|
||||
def extract(args: Value.Sum): Seq[(String, Any)] =
|
||||
args match {
|
||||
case x: Value.Sum.Record => x.value.fields.flatMap(flatten(prefix :+ field.label, _))
|
||||
case x: Value.Sum.Variant => x.value.value.toList.map(_.sum).flatMap(extract)
|
||||
case x => Seq(((prefix :+ field.label).mkString("."), x.value))
|
||||
}
|
||||
field.value.map(_.sum).toList.flatMap(extract)
|
||||
}
|
||||
|
||||
def arguments: Map[String, Any] =
|
||||
event.createArguments.toList.flatMap(_.fields).flatMap(flatten(Seq(), _)).toMap
|
||||
|
||||
def toContractData: ContractData = {
|
||||
val templateId = TemplateId.fromIdentifier(
|
||||
event.templateId.getOrElse(throw new IllegalArgumentException("Template Id not specified"))
|
||||
)
|
||||
val createArguments =
|
||||
event.createArguments.getOrElse(
|
||||
throw new IllegalArgumentException("Create Arguments not specified")
|
||||
)
|
||||
val lfContractId =
|
||||
LfContractId
|
||||
.fromString(event.contractId)
|
||||
.getOrElse(
|
||||
throw new IllegalArgumentException(s"Illegal Contract Id: ${event.contractId}")
|
||||
)
|
||||
|
||||
val contractSaltO = for {
|
||||
fatInstance <- TransactionCoder.decodeFatContractInstance(event.createdEventBlob).toOption
|
||||
parsed = DriverContractMetadata.fromByteString(fatInstance.cantonData.toByteString)
|
||||
} yield parsed.fold[Salt](
|
||||
err =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Could not deserialize driver contract metadata: ${err.message}"
|
||||
),
|
||||
_.salt,
|
||||
)
|
||||
|
||||
val ledgerCreateTimeO =
|
||||
event.createdAt.map(TimestampConversion.toLf(_, TimestampConversion.ConversionMode.Exact))
|
||||
|
||||
ContractData(
|
||||
templateId = templateId,
|
||||
createArguments = createArguments,
|
||||
signatories = event.signatories.toSet,
|
||||
observers = event.observers.toSet,
|
||||
inheritedContractId = lfContractId,
|
||||
contractSalt = contractSaltO,
|
||||
ledgerCreateTime = ledgerCreateTimeO,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Holder of "core" contract defining fields (particularly those relevant for importing contracts) */
|
||||
final case class ContractData(
|
||||
templateId: TemplateId,
|
||||
createArguments: Record,
|
||||
// track signatories and observers for use as auth validation by daml engine
|
||||
signatories: Set[String],
|
||||
observers: Set[String],
|
||||
inheritedContractId: LfContractId,
|
||||
contractSalt: Option[Salt],
|
||||
ledgerCreateTime: Option[Time.Timestamp],
|
||||
)
|
||||
|
||||
}
|
@ -0,0 +1,849 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import com.daml.ledger.api.v1.command_completion_service.Checkpoint
|
||||
import com.daml.ledger.api.v1.commands.{Command, DisclosedContract}
|
||||
import com.daml.ledger.api.v1.event_query_service.GetEventsByContractIdRequest
|
||||
import com.daml.ledger.api.v1.transaction_filter.{Filters, InclusiveFilters, TemplateFilter}
|
||||
import com.daml.ledger.api.v2.command_completion_service.CommandCompletionServiceGrpc.CommandCompletionServiceStub
|
||||
import com.daml.ledger.api.v2.command_completion_service.{
|
||||
CommandCompletionServiceGrpc,
|
||||
CompletionStreamRequest,
|
||||
CompletionStreamResponse,
|
||||
}
|
||||
import com.daml.ledger.api.v2.command_service.CommandServiceGrpc.CommandServiceStub
|
||||
import com.daml.ledger.api.v2.command_service.{
|
||||
CommandServiceGrpc,
|
||||
SubmitAndWaitForTransactionResponse,
|
||||
SubmitAndWaitForTransactionTreeResponse,
|
||||
SubmitAndWaitRequest,
|
||||
}
|
||||
import com.daml.ledger.api.v2.command_submission_service.CommandSubmissionServiceGrpc.CommandSubmissionServiceStub
|
||||
import com.daml.ledger.api.v2.command_submission_service.{
|
||||
CommandSubmissionServiceGrpc,
|
||||
SubmitReassignmentRequest,
|
||||
SubmitReassignmentResponse,
|
||||
SubmitRequest,
|
||||
SubmitResponse,
|
||||
}
|
||||
import com.daml.ledger.api.v2.commands.Commands
|
||||
import com.daml.ledger.api.v2.completion.Completion
|
||||
import com.daml.ledger.api.v2.event_query_service.EventQueryServiceGrpc.EventQueryServiceStub
|
||||
import com.daml.ledger.api.v2.event_query_service.{
|
||||
EventQueryServiceGrpc,
|
||||
GetEventsByContractIdResponse,
|
||||
}
|
||||
import com.daml.ledger.api.v2.participant_offset.ParticipantOffset
|
||||
import com.daml.ledger.api.v2.reassignment.{AssignedEvent, Reassignment, UnassignedEvent}
|
||||
import com.daml.ledger.api.v2.reassignment_command.{
|
||||
AssignCommand,
|
||||
ReassignmentCommand,
|
||||
UnassignCommand,
|
||||
}
|
||||
import com.daml.ledger.api.v2.state_service.StateServiceGrpc.StateServiceStub
|
||||
import com.daml.ledger.api.v2.state_service.{
|
||||
GetActiveContractsRequest,
|
||||
GetActiveContractsResponse,
|
||||
GetConnectedDomainsRequest,
|
||||
GetConnectedDomainsResponse,
|
||||
GetLedgerEndRequest,
|
||||
GetLedgerEndResponse,
|
||||
StateServiceGrpc,
|
||||
}
|
||||
import com.daml.ledger.api.v2.testing.time_service.TimeServiceGrpc.TimeServiceStub
|
||||
import com.daml.ledger.api.v2.testing.time_service.{
|
||||
GetTimeRequest,
|
||||
GetTimeResponse,
|
||||
SetTimeRequest,
|
||||
TimeServiceGrpc,
|
||||
}
|
||||
import com.daml.ledger.api.v2.transaction.{Transaction, TransactionTree}
|
||||
import com.daml.ledger.api.v2.transaction_filter.TransactionFilter
|
||||
import com.daml.ledger.api.v2.update_service.UpdateServiceGrpc.UpdateServiceStub
|
||||
import com.daml.ledger.api.v2.update_service.{
|
||||
GetTransactionByIdRequest,
|
||||
GetTransactionTreeResponse,
|
||||
GetUpdateTreesResponse,
|
||||
GetUpdatesRequest,
|
||||
GetUpdatesResponse,
|
||||
UpdateServiceGrpc,
|
||||
}
|
||||
import com.digitalasset.canton.LfPartyId
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
DefaultUnboundedTimeout,
|
||||
ServerEnforcedTimeout,
|
||||
TimeoutType,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.data.TemplateId
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.ledger.api.DeduplicationPeriod
|
||||
import com.digitalasset.canton.logging.ErrorLoggingContext
|
||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.networking.grpc.ForwardingStreamObserver
|
||||
import com.digitalasset.canton.protocol.LfContractId
|
||||
import com.digitalasset.canton.serialization.ProtoConverter
|
||||
import com.digitalasset.canton.topology.DomainId
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.*
|
||||
import io.grpc.stub.StreamObserver
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
// TODO(#15280) delete LedgerApiCommands, and rename this to LedgerApiCommands
|
||||
object LedgerApiV2Commands {
|
||||
|
||||
object UpdateService {
|
||||
|
||||
sealed trait UpdateTreeWrapper
|
||||
sealed trait UpdateWrapper
|
||||
final case class TransactionTreeWrapper(transactionTree: TransactionTree)
|
||||
extends UpdateTreeWrapper
|
||||
final case class TransactionWrapper(transaction: Transaction) extends UpdateWrapper
|
||||
sealed trait ReassignmentWrapper extends UpdateTreeWrapper with UpdateWrapper {
|
||||
def reassignment: Reassignment
|
||||
}
|
||||
object ReassignmentWrapper {
|
||||
def apply(reassignment: Reassignment): ReassignmentWrapper = {
|
||||
val event = reassignment.event
|
||||
event.assignedEvent
|
||||
.map[ReassignmentWrapper](AssignedWrapper(reassignment, _))
|
||||
.orElse(
|
||||
event.unassignedEvent.map[ReassignmentWrapper](UnassignedWrapper(reassignment, _))
|
||||
)
|
||||
.getOrElse(
|
||||
throw new IllegalStateException(
|
||||
s"Invalid reassignment event (only supported UnassignedEvent and AssignedEvent): ${reassignment.event}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
final case class AssignedWrapper(reassignment: Reassignment, assignedEvent: AssignedEvent)
|
||||
extends ReassignmentWrapper
|
||||
final case class UnassignedWrapper(reassignment: Reassignment, unassignedEvent: UnassignedEvent)
|
||||
extends ReassignmentWrapper
|
||||
|
||||
trait BaseCommand[Req, Resp, Res] extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = UpdateServiceStub
|
||||
|
||||
override def createService(channel: ManagedChannel): UpdateServiceStub =
|
||||
UpdateServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
trait SubscribeBase[Resp, Res]
|
||||
extends BaseCommand[GetUpdatesRequest, AutoCloseable, AutoCloseable] {
|
||||
// The subscription should never be cut short because of a gRPC timeout
|
||||
override def timeoutType: TimeoutType = ServerEnforcedTimeout
|
||||
|
||||
def observer: StreamObserver[Res]
|
||||
|
||||
def beginExclusive: ParticipantOffset
|
||||
|
||||
def endInclusive: Option[ParticipantOffset]
|
||||
|
||||
def filter: TransactionFilter
|
||||
|
||||
def verbose: Boolean
|
||||
|
||||
def doRequest(
|
||||
service: UpdateServiceStub,
|
||||
request: GetUpdatesRequest,
|
||||
rawObserver: StreamObserver[Resp],
|
||||
): Unit
|
||||
|
||||
def extractResults(response: Resp): IterableOnce[Res]
|
||||
|
||||
implicit def loggingContext: ErrorLoggingContext
|
||||
|
||||
override def createRequest(): Either[String, GetUpdatesRequest] = Right {
|
||||
GetUpdatesRequest(
|
||||
beginExclusive = Some(beginExclusive),
|
||||
endInclusive = endInclusive,
|
||||
verbose = verbose,
|
||||
filter = Some(filter),
|
||||
)
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: UpdateServiceStub,
|
||||
request: GetUpdatesRequest,
|
||||
): Future[AutoCloseable] = {
|
||||
val rawObserver = new ForwardingStreamObserver[Resp, Res](observer, extractResults)
|
||||
val context = Context.current().withCancellation()
|
||||
context.run(() => doRequest(service, request, rawObserver))
|
||||
Future.successful(context)
|
||||
}
|
||||
|
||||
override def handleResponse(response: AutoCloseable): Either[String, AutoCloseable] = Right(
|
||||
response
|
||||
)
|
||||
}
|
||||
|
||||
final case class SubscribeTrees(
|
||||
override val observer: StreamObserver[UpdateTreeWrapper],
|
||||
override val beginExclusive: ParticipantOffset,
|
||||
override val endInclusive: Option[ParticipantOffset],
|
||||
override val filter: TransactionFilter,
|
||||
override val verbose: Boolean,
|
||||
)(override implicit val loggingContext: ErrorLoggingContext)
|
||||
extends SubscribeBase[GetUpdateTreesResponse, UpdateTreeWrapper] {
|
||||
override def doRequest(
|
||||
service: UpdateServiceStub,
|
||||
request: GetUpdatesRequest,
|
||||
rawObserver: StreamObserver[GetUpdateTreesResponse],
|
||||
): Unit =
|
||||
service.getUpdateTrees(request, rawObserver)
|
||||
|
||||
override def extractResults(
|
||||
response: GetUpdateTreesResponse
|
||||
): IterableOnce[UpdateTreeWrapper] =
|
||||
response.update.transactionTree
|
||||
.map[UpdateTreeWrapper](TransactionTreeWrapper)
|
||||
.orElse(response.update.reassignment.map(ReassignmentWrapper(_)))
|
||||
}
|
||||
|
||||
final case class SubscribeFlat(
|
||||
override val observer: StreamObserver[UpdateWrapper],
|
||||
override val beginExclusive: ParticipantOffset,
|
||||
override val endInclusive: Option[ParticipantOffset],
|
||||
override val filter: TransactionFilter,
|
||||
override val verbose: Boolean,
|
||||
)(override implicit val loggingContext: ErrorLoggingContext)
|
||||
extends SubscribeBase[GetUpdatesResponse, UpdateWrapper] {
|
||||
override def doRequest(
|
||||
service: UpdateServiceStub,
|
||||
request: GetUpdatesRequest,
|
||||
rawObserver: StreamObserver[GetUpdatesResponse],
|
||||
): Unit =
|
||||
service.getUpdates(request, rawObserver)
|
||||
|
||||
override def extractResults(response: GetUpdatesResponse): IterableOnce[UpdateWrapper] =
|
||||
response.update.transaction
|
||||
.map[UpdateWrapper](TransactionWrapper)
|
||||
.orElse(response.update.reassignment.map(ReassignmentWrapper(_)))
|
||||
}
|
||||
|
||||
final case class GetTransactionById(parties: Set[LfPartyId], id: String)(implicit
|
||||
ec: ExecutionContext
|
||||
) extends BaseCommand[GetTransactionByIdRequest, GetTransactionTreeResponse, Option[
|
||||
TransactionTree
|
||||
]]
|
||||
with PrettyPrinting {
|
||||
override def createRequest(): Either[String, GetTransactionByIdRequest] = Right {
|
||||
GetTransactionByIdRequest(
|
||||
updateId = id,
|
||||
requestingParties = parties.toSeq,
|
||||
)
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: UpdateServiceStub,
|
||||
request: GetTransactionByIdRequest,
|
||||
): Future[GetTransactionTreeResponse] = {
|
||||
// The Ledger API will throw an error if it can't find a transaction by ID.
|
||||
// However, as Canton is distributed, a transaction ID might show up later, so we don't treat this as
|
||||
// an error and change it to a None
|
||||
service.getTransactionTreeById(request).recover {
|
||||
case e: StatusRuntimeException if e.getStatus.getCode == Status.Code.NOT_FOUND =>
|
||||
GetTransactionTreeResponse(None)
|
||||
}
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: GetTransactionTreeResponse
|
||||
): Either[String, Option[TransactionTree]] =
|
||||
Right(response.transaction)
|
||||
|
||||
override def pretty: Pretty[GetTransactionById] =
|
||||
prettyOfClass(
|
||||
param("id", _.id.unquoted),
|
||||
param("parties", _.parties),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private[commands] trait SubmitCommand extends PrettyPrinting {
|
||||
def actAs: Seq[LfPartyId]
|
||||
def readAs: Seq[LfPartyId]
|
||||
def commands: Seq[Command]
|
||||
def workflowId: String
|
||||
def commandId: String
|
||||
def deduplicationPeriod: Option[DeduplicationPeriod]
|
||||
def submissionId: String
|
||||
def minLedgerTimeAbs: Option[Instant]
|
||||
def disclosedContracts: Seq[DisclosedContract]
|
||||
def domainId: DomainId
|
||||
def applicationId: String
|
||||
|
||||
protected def mkCommand: Commands = Commands(
|
||||
workflowId = workflowId,
|
||||
applicationId = applicationId,
|
||||
commandId = if (commandId.isEmpty) UUID.randomUUID().toString else commandId,
|
||||
actAs = actAs,
|
||||
readAs = readAs,
|
||||
commands = commands,
|
||||
deduplicationPeriod = deduplicationPeriod.fold(
|
||||
Commands.DeduplicationPeriod.Empty: Commands.DeduplicationPeriod
|
||||
) {
|
||||
case DeduplicationPeriod.DeduplicationDuration(duration) =>
|
||||
Commands.DeduplicationPeriod.DeduplicationDuration(
|
||||
ProtoConverter.DurationConverter.toProtoPrimitive(duration)
|
||||
)
|
||||
case DeduplicationPeriod.DeduplicationOffset(offset) =>
|
||||
Commands.DeduplicationPeriod.DeduplicationOffset(
|
||||
offset.toHexString
|
||||
)
|
||||
},
|
||||
minLedgerTimeAbs = minLedgerTimeAbs.map(ProtoConverter.InstantConverter.toProtoPrimitive),
|
||||
submissionId = submissionId,
|
||||
disclosedContracts = disclosedContracts,
|
||||
domainId = domainId.toProtoPrimitive,
|
||||
)
|
||||
|
||||
override def pretty: Pretty[this.type] =
|
||||
prettyOfClass(
|
||||
param("actAs", _.actAs),
|
||||
param("readAs", _.readAs),
|
||||
param("commandId", _.commandId.singleQuoted),
|
||||
param("workflowId", _.workflowId.singleQuoted),
|
||||
param("submissionId", _.submissionId.singleQuoted),
|
||||
param("deduplicationPeriod", _.deduplicationPeriod),
|
||||
paramIfDefined("minLedgerTimeAbs", _.minLedgerTimeAbs),
|
||||
paramWithoutValue("commands"),
|
||||
)
|
||||
}
|
||||
|
||||
object CommandSubmissionService {
|
||||
trait BaseCommand[Req, Resp, Res] extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = CommandSubmissionServiceStub
|
||||
override def createService(channel: ManagedChannel): CommandSubmissionServiceStub =
|
||||
CommandSubmissionServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case class Submit(
|
||||
override val actAs: Seq[LfPartyId],
|
||||
override val readAs: Seq[LfPartyId],
|
||||
override val commands: Seq[Command],
|
||||
override val workflowId: String,
|
||||
override val commandId: String,
|
||||
override val deduplicationPeriod: Option[DeduplicationPeriod],
|
||||
override val submissionId: String,
|
||||
override val minLedgerTimeAbs: Option[Instant],
|
||||
override val disclosedContracts: Seq[DisclosedContract],
|
||||
override val domainId: DomainId,
|
||||
override val applicationId: String,
|
||||
) extends SubmitCommand
|
||||
with BaseCommand[SubmitRequest, SubmitResponse, Unit] {
|
||||
override def createRequest(): Either[String, SubmitRequest] = Right(
|
||||
SubmitRequest(commands = Some(mkCommand))
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: CommandSubmissionServiceStub,
|
||||
request: SubmitRequest,
|
||||
): Future[SubmitResponse] = {
|
||||
service.submit(request)
|
||||
}
|
||||
|
||||
override def handleResponse(response: SubmitResponse): Either[String, Unit] = Right(())
|
||||
}
|
||||
|
||||
final case class SubmitAssignCommand(
|
||||
workflowId: String,
|
||||
applicationId: String,
|
||||
commandId: String,
|
||||
submitter: LfPartyId,
|
||||
submissionId: String,
|
||||
unassignId: String,
|
||||
source: DomainId,
|
||||
target: DomainId,
|
||||
) extends BaseCommand[SubmitReassignmentRequest, SubmitReassignmentResponse, Unit] {
|
||||
override def createRequest(): Either[String, SubmitReassignmentRequest] = Right(
|
||||
SubmitReassignmentRequest(
|
||||
Some(
|
||||
ReassignmentCommand(
|
||||
workflowId = workflowId,
|
||||
applicationId = applicationId,
|
||||
commandId = commandId,
|
||||
submitter = submitter.toString,
|
||||
command = ReassignmentCommand.Command.AssignCommand(
|
||||
AssignCommand(
|
||||
unassignId = unassignId,
|
||||
source = source.toProtoPrimitive,
|
||||
target = target.toProtoPrimitive,
|
||||
)
|
||||
),
|
||||
submissionId = submissionId,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: CommandSubmissionServiceStub,
|
||||
request: SubmitReassignmentRequest,
|
||||
): Future[SubmitReassignmentResponse] = {
|
||||
service.submitReassignment(request)
|
||||
}
|
||||
|
||||
override def handleResponse(response: SubmitReassignmentResponse): Either[String, Unit] =
|
||||
Right(())
|
||||
}
|
||||
|
||||
final case class SubmitUnassignCommand(
|
||||
workflowId: String,
|
||||
applicationId: String,
|
||||
commandId: String,
|
||||
submitter: LfPartyId,
|
||||
submissionId: String,
|
||||
contractId: LfContractId,
|
||||
source: DomainId,
|
||||
target: DomainId,
|
||||
) extends BaseCommand[SubmitReassignmentRequest, SubmitReassignmentResponse, Unit] {
|
||||
override def createRequest(): Either[String, SubmitReassignmentRequest] = Right(
|
||||
SubmitReassignmentRequest(
|
||||
Some(
|
||||
ReassignmentCommand(
|
||||
workflowId = workflowId,
|
||||
applicationId = applicationId,
|
||||
commandId = commandId,
|
||||
submitter = submitter.toString,
|
||||
command = ReassignmentCommand.Command.UnassignCommand(
|
||||
UnassignCommand(
|
||||
contractId = contractId.coid.toString,
|
||||
source = source.toProtoPrimitive,
|
||||
target = target.toProtoPrimitive,
|
||||
)
|
||||
),
|
||||
submissionId = submissionId,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: CommandSubmissionServiceStub,
|
||||
request: SubmitReassignmentRequest,
|
||||
): Future[SubmitReassignmentResponse] = {
|
||||
service.submitReassignment(request)
|
||||
}
|
||||
|
||||
override def handleResponse(response: SubmitReassignmentResponse): Either[String, Unit] =
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
object CommandService {
|
||||
trait BaseCommand[Req, Resp, Res] extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = CommandServiceStub
|
||||
override def createService(channel: ManagedChannel): CommandServiceStub =
|
||||
CommandServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case class SubmitAndWaitTransactionTree(
|
||||
override val actAs: Seq[LfPartyId],
|
||||
override val readAs: Seq[LfPartyId],
|
||||
override val commands: Seq[Command],
|
||||
override val workflowId: String,
|
||||
override val commandId: String,
|
||||
override val deduplicationPeriod: Option[DeduplicationPeriod],
|
||||
override val submissionId: String,
|
||||
override val minLedgerTimeAbs: Option[Instant],
|
||||
override val disclosedContracts: Seq[DisclosedContract],
|
||||
override val domainId: DomainId,
|
||||
override val applicationId: String,
|
||||
) extends SubmitCommand
|
||||
with BaseCommand[
|
||||
SubmitAndWaitRequest,
|
||||
SubmitAndWaitForTransactionTreeResponse,
|
||||
TransactionTree,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, SubmitAndWaitRequest] =
|
||||
Right(SubmitAndWaitRequest(commands = Some(mkCommand)))
|
||||
|
||||
override def submitRequest(
|
||||
service: CommandServiceStub,
|
||||
request: SubmitAndWaitRequest,
|
||||
): Future[SubmitAndWaitForTransactionTreeResponse] =
|
||||
service.submitAndWaitForTransactionTree(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: SubmitAndWaitForTransactionTreeResponse
|
||||
): Either[String, TransactionTree] =
|
||||
response.transaction.toRight("Received response without any transaction tree")
|
||||
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
final case class SubmitAndWaitTransaction(
|
||||
override val actAs: Seq[LfPartyId],
|
||||
override val readAs: Seq[LfPartyId],
|
||||
override val commands: Seq[Command],
|
||||
override val workflowId: String,
|
||||
override val commandId: String,
|
||||
override val deduplicationPeriod: Option[DeduplicationPeriod],
|
||||
override val submissionId: String,
|
||||
override val minLedgerTimeAbs: Option[Instant],
|
||||
override val disclosedContracts: Seq[DisclosedContract],
|
||||
override val domainId: DomainId,
|
||||
override val applicationId: String,
|
||||
) extends SubmitCommand
|
||||
with BaseCommand[SubmitAndWaitRequest, SubmitAndWaitForTransactionResponse, Transaction] {
|
||||
|
||||
override def createRequest(): Either[String, SubmitAndWaitRequest] =
|
||||
Right(SubmitAndWaitRequest(commands = Some(mkCommand)))
|
||||
|
||||
override def submitRequest(
|
||||
service: CommandServiceStub,
|
||||
request: SubmitAndWaitRequest,
|
||||
): Future[SubmitAndWaitForTransactionResponse] =
|
||||
service.submitAndWaitForTransaction(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: SubmitAndWaitForTransactionResponse
|
||||
): Either[String, Transaction] =
|
||||
response.transaction.toRight("Received response without any transaction")
|
||||
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
object StateService {
|
||||
abstract class BaseCommand[Req, Resp, Res] extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = StateServiceStub
|
||||
|
||||
override def createService(channel: ManagedChannel): StateServiceStub =
|
||||
StateServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case class LedgerEnd()
|
||||
extends BaseCommand[GetLedgerEndRequest, GetLedgerEndResponse, ParticipantOffset] {
|
||||
|
||||
override def createRequest(): Either[String, GetLedgerEndRequest] =
|
||||
Right(GetLedgerEndRequest())
|
||||
|
||||
override def submitRequest(
|
||||
service: StateServiceStub,
|
||||
request: GetLedgerEndRequest,
|
||||
): Future[GetLedgerEndResponse] =
|
||||
service.getLedgerEnd(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: GetLedgerEndResponse
|
||||
): Either[String, ParticipantOffset] =
|
||||
response.offset.toRight("Empty LedgerEndResponse received without offset")
|
||||
}
|
||||
|
||||
final case class GetConnectedDomains(partyId: LfPartyId)
|
||||
extends BaseCommand[
|
||||
GetConnectedDomainsRequest,
|
||||
GetConnectedDomainsResponse,
|
||||
GetConnectedDomainsResponse,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, GetConnectedDomainsRequest] =
|
||||
Right(GetConnectedDomainsRequest(partyId.toString))
|
||||
|
||||
override def submitRequest(
|
||||
service: StateServiceStub,
|
||||
request: GetConnectedDomainsRequest,
|
||||
): Future[GetConnectedDomainsResponse] =
|
||||
service.getConnectedDomains(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: GetConnectedDomainsResponse
|
||||
): Either[String, GetConnectedDomainsResponse] =
|
||||
Right(response)
|
||||
}
|
||||
|
||||
final case class GetActiveContracts(
|
||||
parties: Set[LfPartyId],
|
||||
limit: PositiveInt,
|
||||
templateFilter: Seq[TemplateId] = Seq.empty,
|
||||
activeAtOffset: String = "",
|
||||
verbose: Boolean = true,
|
||||
timeout: FiniteDuration,
|
||||
includeCreatedEventBlob: Boolean = false,
|
||||
)(scheduler: ScheduledExecutorService)
|
||||
extends BaseCommand[GetActiveContractsRequest, Seq[GetActiveContractsResponse], Seq[
|
||||
GetActiveContractsResponse
|
||||
]] {
|
||||
|
||||
override def createRequest(): Either[String, GetActiveContractsRequest] = {
|
||||
val filter =
|
||||
if (templateFilter.nonEmpty) {
|
||||
Filters(
|
||||
Some(
|
||||
InclusiveFilters(templateFilters =
|
||||
templateFilter.map(tId =>
|
||||
TemplateFilter(Some(tId.toIdentifier), includeCreatedEventBlob)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else Filters.defaultInstance
|
||||
Right(
|
||||
GetActiveContractsRequest(
|
||||
filter = Some(TransactionFilter(parties.map((_, filter)).toMap)),
|
||||
verbose = verbose,
|
||||
activeAtOffset = activeAtOffset,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: StateServiceStub,
|
||||
request: GetActiveContractsRequest,
|
||||
): Future[Seq[GetActiveContractsResponse]] = {
|
||||
GrpcAdminCommand.streamedResponse[
|
||||
GetActiveContractsRequest,
|
||||
GetActiveContractsResponse,
|
||||
GetActiveContractsResponse,
|
||||
](
|
||||
service.getActiveContracts,
|
||||
List(_),
|
||||
request,
|
||||
limit.value,
|
||||
timeout,
|
||||
scheduler,
|
||||
)
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: Seq[GetActiveContractsResponse]
|
||||
): Either[String, Seq[GetActiveContractsResponse]] = {
|
||||
Right(response)
|
||||
}
|
||||
|
||||
// fetching ACS might take long if we fetch a lot of data
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
final case class CompletionWrapper(
|
||||
completion: Completion,
|
||||
checkpoint: Checkpoint,
|
||||
domainId: DomainId,
|
||||
)
|
||||
|
||||
object CommandCompletionService {
|
||||
abstract class BaseCommand[Req, Resp, Res] extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = CommandCompletionServiceStub
|
||||
|
||||
override def createService(channel: ManagedChannel): CommandCompletionServiceStub =
|
||||
CommandCompletionServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case class CompletionRequest(
|
||||
partyId: LfPartyId,
|
||||
beginOffset: ParticipantOffset,
|
||||
expectedCompletions: Int,
|
||||
timeout: java.time.Duration,
|
||||
applicationId: String,
|
||||
)(filter: CompletionWrapper => Boolean, scheduler: ScheduledExecutorService)
|
||||
extends BaseCommand[
|
||||
CompletionStreamRequest,
|
||||
Seq[CompletionWrapper],
|
||||
Seq[CompletionWrapper],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, CompletionStreamRequest] =
|
||||
Right(
|
||||
CompletionStreamRequest(
|
||||
applicationId = applicationId,
|
||||
parties = Seq(partyId),
|
||||
beginExclusive = Some(beginOffset),
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: CommandCompletionServiceStub,
|
||||
request: CompletionStreamRequest,
|
||||
): Future[Seq[CompletionWrapper]] = {
|
||||
import scala.jdk.DurationConverters.*
|
||||
GrpcAdminCommand
|
||||
.streamedResponse[CompletionStreamRequest, CompletionStreamResponse, CompletionWrapper](
|
||||
service.completionStream,
|
||||
response =>
|
||||
List(
|
||||
CompletionWrapper(
|
||||
completion = response.completion.getOrElse(
|
||||
throw new IllegalStateException("Completion should be present.")
|
||||
),
|
||||
checkpoint = response.checkpoint.getOrElse(
|
||||
throw new IllegalStateException("Checkpoint should be present.")
|
||||
),
|
||||
domainId = DomainId.tryFromString(response.domainId),
|
||||
)
|
||||
).filter(filter),
|
||||
request,
|
||||
expectedCompletions,
|
||||
timeout.toScala,
|
||||
scheduler,
|
||||
)
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: Seq[CompletionWrapper]
|
||||
): Either[String, Seq[CompletionWrapper]] =
|
||||
Right(response)
|
||||
|
||||
override def timeoutType: TimeoutType = ServerEnforcedTimeout
|
||||
}
|
||||
|
||||
final case class Subscribe(
|
||||
observer: StreamObserver[CompletionWrapper],
|
||||
parties: Seq[String],
|
||||
offset: Option[ParticipantOffset],
|
||||
applicationId: String,
|
||||
)(implicit loggingContext: ErrorLoggingContext)
|
||||
extends BaseCommand[CompletionStreamRequest, AutoCloseable, AutoCloseable] {
|
||||
// The subscription should never be cut short because of a gRPC timeout
|
||||
override def timeoutType: TimeoutType = ServerEnforcedTimeout
|
||||
|
||||
override def createRequest(): Either[String, CompletionStreamRequest] = Right {
|
||||
CompletionStreamRequest(
|
||||
applicationId = applicationId,
|
||||
parties = parties,
|
||||
beginExclusive = offset,
|
||||
)
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: CommandCompletionServiceStub,
|
||||
request: CompletionStreamRequest,
|
||||
): Future[AutoCloseable] = {
|
||||
val rawObserver = new ForwardingStreamObserver[CompletionStreamResponse, CompletionWrapper](
|
||||
observer,
|
||||
response =>
|
||||
List(
|
||||
CompletionWrapper(
|
||||
completion = response.completion.getOrElse(
|
||||
throw new IllegalStateException("Completion should be present.")
|
||||
),
|
||||
checkpoint = response.checkpoint.getOrElse(
|
||||
throw new IllegalStateException("Checkpoint should be present.")
|
||||
),
|
||||
domainId = DomainId.tryFromString(response.domainId),
|
||||
)
|
||||
),
|
||||
)
|
||||
val context = Context.current().withCancellation()
|
||||
context.run(() => service.completionStream(request, rawObserver))
|
||||
Future.successful(context)
|
||||
}
|
||||
|
||||
override def handleResponse(response: AutoCloseable): Either[String, AutoCloseable] = Right(
|
||||
response
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object Time {
|
||||
abstract class BaseCommand[Req, Resp, Res] extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = TimeServiceStub
|
||||
|
||||
override def createService(channel: ManagedChannel): TimeServiceStub =
|
||||
TimeServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final object Get
|
||||
extends BaseCommand[
|
||||
GetTimeRequest,
|
||||
GetTimeResponse,
|
||||
CantonTimestamp,
|
||||
] {
|
||||
|
||||
override def submitRequest(
|
||||
service: TimeServiceStub,
|
||||
request: GetTimeRequest,
|
||||
): Future[GetTimeResponse] = {
|
||||
service.getTime(request)
|
||||
}
|
||||
|
||||
/** Create the request from configured options
|
||||
*/
|
||||
override def createRequest(): Either[String, GetTimeRequest] = Right(GetTimeRequest())
|
||||
|
||||
/** Handle the response the service has provided
|
||||
*/
|
||||
override def handleResponse(
|
||||
response: GetTimeResponse
|
||||
): Either[String, CantonTimestamp] =
|
||||
for {
|
||||
prototTimestamp <- response.currentTime.map(Right(_)).getOrElse(Left("currentTime empty"))
|
||||
result <- CantonTimestamp.fromProtoPrimitive(prototTimestamp).left.map(_.message)
|
||||
} yield result
|
||||
}
|
||||
|
||||
final case class Set(currentTime: CantonTimestamp, newTime: CantonTimestamp)
|
||||
extends BaseCommand[
|
||||
SetTimeRequest,
|
||||
Empty,
|
||||
Unit,
|
||||
] {
|
||||
|
||||
override def submitRequest(service: TimeServiceStub, request: SetTimeRequest): Future[Empty] =
|
||||
service.setTime(request)
|
||||
|
||||
override def createRequest(): Either[String, SetTimeRequest] =
|
||||
Right(
|
||||
SetTimeRequest(
|
||||
currentTime = Some(currentTime.toProtoPrimitive),
|
||||
newTime = Some(newTime.toProtoPrimitive),
|
||||
)
|
||||
)
|
||||
|
||||
/** Handle the response the service has provided
|
||||
*/
|
||||
override def handleResponse(response: Empty): Either[String, Unit] = Right(())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object QueryService {
|
||||
|
||||
abstract class BaseCommand[Req, Res] extends GrpcAdminCommand[Req, Res, Res] {
|
||||
override type Svc = EventQueryServiceStub
|
||||
|
||||
override def createService(channel: ManagedChannel): EventQueryServiceStub =
|
||||
EventQueryServiceGrpc.stub(channel)
|
||||
|
||||
override def handleResponse(response: Res): Either[String, Res] = Right(response)
|
||||
}
|
||||
|
||||
final case class GetEventsByContractId(
|
||||
contractId: String,
|
||||
requestingParties: Seq[String],
|
||||
) extends BaseCommand[
|
||||
GetEventsByContractIdRequest,
|
||||
GetEventsByContractIdResponse,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, GetEventsByContractIdRequest] = Right(
|
||||
GetEventsByContractIdRequest(
|
||||
contractId = contractId,
|
||||
requestingParties = requestingParties,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: EventQueryServiceStub,
|
||||
request: GetEventsByContractIdRequest,
|
||||
): Future[GetEventsByContractIdResponse] = service.getEventsByContractId(request)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import com.digitalasset.canton.admin.api.client.data.PruningSchedule
|
||||
import com.digitalasset.canton.config.PositiveDurationSeconds
|
||||
import com.digitalasset.canton.pruning.admin.v0
|
||||
import com.digitalasset.canton.pruning.admin.v0.{PruningSchedule as PruningScheduleP, *}
|
||||
import io.grpc.ManagedChannel
|
||||
import io.grpc.stub.AbstractStub
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/** Exposes shared grpc client pruning scheduler commands reusable by participant/mediator/sequencer
|
||||
* admin api.
|
||||
* Having to type-parameterize as grpc does not support inheritance and passing in the grpc stub methods in one by one
|
||||
*/
|
||||
class PruningSchedulerCommands[Stub <: AbstractStub[Stub]](
|
||||
createServiceStub: ManagedChannel => Stub,
|
||||
submitSetSchedule: (Stub, SetSchedule.Request) => Future[SetSchedule.Response],
|
||||
submitClearSchedule: (Stub, ClearSchedule.Request) => Future[ClearSchedule.Response],
|
||||
submitSetCron: (Stub, SetCron.Request) => Future[SetCron.Response],
|
||||
submitSetMaxDuration: (Stub, v0.SetMaxDuration.Request) => Future[SetMaxDuration.Response],
|
||||
submitSetRetention: (Stub, SetRetention.Request) => Future[SetRetention.Response],
|
||||
submitGetSchedule: (Stub, GetSchedule.Request) => Future[GetSchedule.Response],
|
||||
) {
|
||||
abstract class BaseCommand[Req, Res, Ret] extends GrpcAdminCommand[Req, Res, Ret] {
|
||||
override type Svc = Stub
|
||||
override def createService(channel: ManagedChannel): Svc = createServiceStub(channel)
|
||||
}
|
||||
|
||||
// case classes not final as the scala compiler can't check outer Svc type reference
|
||||
case class SetScheduleCommand(
|
||||
cron: String,
|
||||
maxDuration: PositiveDurationSeconds,
|
||||
retention: PositiveDurationSeconds,
|
||||
) extends BaseCommand[SetSchedule.Request, SetSchedule.Response, Unit] {
|
||||
override def createRequest(): Right[String, SetSchedule.Request] =
|
||||
Right(
|
||||
SetSchedule.Request(
|
||||
Some(
|
||||
PruningScheduleP(
|
||||
cron,
|
||||
Some(maxDuration.toProtoPrimitive),
|
||||
Some(retention.toProtoPrimitive),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: Svc,
|
||||
request: SetSchedule.Request,
|
||||
): Future[SetSchedule.Response] = submitSetSchedule(service, request)
|
||||
|
||||
override def handleResponse(response: SetSchedule.Response): Either[String, Unit] =
|
||||
response match {
|
||||
case SetSchedule.Response() => Right(())
|
||||
}
|
||||
}
|
||||
|
||||
case class ClearScheduleCommand()
|
||||
extends BaseCommand[ClearSchedule.Request, ClearSchedule.Response, Unit] {
|
||||
override def createRequest(): Right[String, ClearSchedule.Request] =
|
||||
Right(ClearSchedule.Request())
|
||||
|
||||
override def submitRequest(
|
||||
service: Svc,
|
||||
request: ClearSchedule.Request,
|
||||
): Future[ClearSchedule.Response] =
|
||||
submitClearSchedule(service, request)
|
||||
|
||||
override def handleResponse(response: ClearSchedule.Response): Either[String, Unit] =
|
||||
response match {
|
||||
case ClearSchedule.Response() => Right(())
|
||||
}
|
||||
}
|
||||
|
||||
case class SetCronCommand(cron: String)
|
||||
extends BaseCommand[SetCron.Request, SetCron.Response, Unit] {
|
||||
override def createRequest(): Right[String, SetCron.Request] =
|
||||
Right(SetCron.Request(cron))
|
||||
|
||||
override def submitRequest(
|
||||
service: Svc,
|
||||
request: SetCron.Request,
|
||||
): Future[SetCron.Response] =
|
||||
submitSetCron(service, request)
|
||||
|
||||
override def handleResponse(response: SetCron.Response): Either[String, Unit] =
|
||||
response match {
|
||||
case SetCron.Response() => Right(())
|
||||
}
|
||||
}
|
||||
|
||||
case class SetMaxDurationCommand(maxDuration: PositiveDurationSeconds)
|
||||
extends BaseCommand[SetMaxDuration.Request, SetMaxDuration.Response, Unit] {
|
||||
override def createRequest(): Right[String, SetMaxDuration.Request] =
|
||||
Right(
|
||||
SetMaxDuration.Request(Some(maxDuration.toProtoPrimitive))
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: Svc,
|
||||
request: SetMaxDuration.Request,
|
||||
): Future[SetMaxDuration.Response] =
|
||||
submitSetMaxDuration(service, request)
|
||||
|
||||
override def handleResponse(response: SetMaxDuration.Response): Either[String, Unit] =
|
||||
response match {
|
||||
case SetMaxDuration.Response() => Right(())
|
||||
}
|
||||
}
|
||||
|
||||
case class SetRetentionCommand(retention: PositiveDurationSeconds)
|
||||
extends BaseCommand[SetRetention.Request, SetRetention.Response, Unit] {
|
||||
override def createRequest(): Right[String, SetRetention.Request] =
|
||||
Right(SetRetention.Request(Some(retention.toProtoPrimitive)))
|
||||
|
||||
override def submitRequest(
|
||||
service: Svc,
|
||||
request: SetRetention.Request,
|
||||
): Future[SetRetention.Response] =
|
||||
submitSetRetention(service, request)
|
||||
|
||||
override def handleResponse(response: SetRetention.Response): Either[String, Unit] =
|
||||
response match {
|
||||
case SetRetention.Response() => Right(())
|
||||
}
|
||||
}
|
||||
|
||||
case class GetScheduleCommand()
|
||||
extends BaseCommand[
|
||||
GetSchedule.Request,
|
||||
GetSchedule.Response,
|
||||
Option[PruningSchedule],
|
||||
] {
|
||||
override def createRequest(): Right[String, GetSchedule.Request] =
|
||||
Right(GetSchedule.Request())
|
||||
|
||||
override def submitRequest(
|
||||
service: Svc,
|
||||
request: GetSchedule.Request,
|
||||
): Future[GetSchedule.Response] =
|
||||
submitGetSchedule(service, request)
|
||||
|
||||
override def handleResponse(
|
||||
response: GetSchedule.Response
|
||||
): Either[
|
||||
String,
|
||||
Option[PruningSchedule],
|
||||
] = response.schedule.fold(
|
||||
Right(None): Either[String, Option[PruningSchedule]]
|
||||
)(PruningSchedule.fromProtoV0(_).bimap(_.message, Some(_)))
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.digitalasset.canton.domain.admin.v0.SequencerAdministrationServiceGrpc.SequencerAdministrationServiceStub
|
||||
import com.digitalasset.canton.domain.admin.v0 as adminproto
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.SequencerPruningStatus
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerTrafficStatus
|
||||
import com.digitalasset.canton.topology.Member
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ManagedChannel
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object SequencerAdminCommands {
|
||||
|
||||
abstract class BaseSequencerAdministrationCommands[Req, Rep, Res]
|
||||
extends GrpcAdminCommand[Req, Rep, Res] {
|
||||
override type Svc = SequencerAdministrationServiceStub
|
||||
|
||||
override def createService(channel: ManagedChannel): SequencerAdministrationServiceStub =
|
||||
adminproto.SequencerAdministrationServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case object GetPruningStatus
|
||||
extends BaseSequencerAdministrationCommands[
|
||||
Empty,
|
||||
adminproto.SequencerPruningStatus,
|
||||
SequencerPruningStatus,
|
||||
] {
|
||||
override def createRequest(): Either[String, Empty] = Right(Empty())
|
||||
override def submitRequest(
|
||||
service: SequencerAdministrationServiceStub,
|
||||
request: Empty,
|
||||
): Future[adminproto.SequencerPruningStatus] =
|
||||
service.pruningStatus(request)
|
||||
override def handleResponse(
|
||||
response: adminproto.SequencerPruningStatus
|
||||
): Either[String, SequencerPruningStatus] =
|
||||
SequencerPruningStatus.fromProtoV0(response).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class GetTrafficControlState(members: Seq[Member])
|
||||
extends BaseSequencerAdministrationCommands[
|
||||
adminproto.TrafficControlStateRequest,
|
||||
adminproto.TrafficControlStateResponse,
|
||||
SequencerTrafficStatus,
|
||||
] {
|
||||
override def createRequest(): Either[String, adminproto.TrafficControlStateRequest] = Right(
|
||||
adminproto.TrafficControlStateRequest(members.map(_.toProtoPrimitive))
|
||||
)
|
||||
override def submitRequest(
|
||||
service: SequencerAdministrationServiceStub,
|
||||
request: adminproto.TrafficControlStateRequest,
|
||||
): Future[adminproto.TrafficControlStateResponse] =
|
||||
service.trafficControlState(request)
|
||||
override def handleResponse(
|
||||
response: adminproto.TrafficControlStateResponse
|
||||
): Either[String, SequencerTrafficStatus] =
|
||||
response.trafficStates
|
||||
.traverse(com.digitalasset.canton.traffic.MemberTrafficStatus.fromProtoV0)
|
||||
.leftMap(_.toString)
|
||||
.map(SequencerTrafficStatus)
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import com.digitalasset.canton.ProtoDeserializationError
|
||||
import com.digitalasset.canton.health.admin.v0.{HealthDumpChunk, HealthDumpRequest}
|
||||
import com.digitalasset.canton.health.admin.{data, v0}
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.Context.CancellableContext
|
||||
import io.grpc.stub.StreamObserver
|
||||
import io.grpc.{Context, ManagedChannel}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object StatusAdminCommands {
|
||||
abstract class GetStatusBase[Result] extends GrpcAdminCommand[Empty, v0.NodeStatus, Result] {
|
||||
override type Svc = v0.StatusServiceGrpc.StatusServiceStub
|
||||
override def createService(channel: ManagedChannel): v0.StatusServiceGrpc.StatusServiceStub =
|
||||
v0.StatusServiceGrpc.stub(channel)
|
||||
override def createRequest(): Either[String, Empty] = Right(Empty())
|
||||
override def submitRequest(
|
||||
service: v0.StatusServiceGrpc.StatusServiceStub,
|
||||
request: Empty,
|
||||
): Future[v0.NodeStatus] =
|
||||
service.status(request)
|
||||
}
|
||||
|
||||
class GetStatus[S <: data.NodeStatus.Status](
|
||||
deserialize: v0.NodeStatus.Status => ParsingResult[S]
|
||||
) extends GetStatusBase[data.NodeStatus[S]] {
|
||||
override def handleResponse(response: v0.NodeStatus): Either[String, data.NodeStatus[S]] =
|
||||
((response.response match {
|
||||
case v0.NodeStatus.Response.NotInitialized(notInitialized) =>
|
||||
Right(data.NodeStatus.NotInitialized(notInitialized.active))
|
||||
case v0.NodeStatus.Response.Success(status) =>
|
||||
deserialize(status).map(data.NodeStatus.Success(_))
|
||||
case v0.NodeStatus.Response.Empty => Left(ProtoDeserializationError.FieldNotSet("response"))
|
||||
}): ParsingResult[data.NodeStatus[S]]).leftMap(_.toString)
|
||||
}
|
||||
|
||||
class GetHealthDump(
|
||||
observer: StreamObserver[HealthDumpChunk],
|
||||
chunkSize: Option[Int],
|
||||
) extends GrpcAdminCommand[HealthDumpRequest, CancellableContext, CancellableContext] {
|
||||
override type Svc = v0.StatusServiceGrpc.StatusServiceStub
|
||||
override def createService(channel: ManagedChannel): v0.StatusServiceGrpc.StatusServiceStub =
|
||||
v0.StatusServiceGrpc.stub(channel)
|
||||
override def submitRequest(
|
||||
service: v0.StatusServiceGrpc.StatusServiceStub,
|
||||
request: HealthDumpRequest,
|
||||
): Future[CancellableContext] = {
|
||||
val context = Context.current().withCancellation()
|
||||
context.run(() => service.healthDump(request, observer))
|
||||
Future.successful(context)
|
||||
}
|
||||
override def createRequest(): Either[String, HealthDumpRequest] = Right(
|
||||
HealthDumpRequest(chunkSize)
|
||||
)
|
||||
override def handleResponse(response: CancellableContext): Either[String, CancellableContext] =
|
||||
Right(response)
|
||||
|
||||
override def timeoutType: GrpcAdminCommand.TimeoutType =
|
||||
GrpcAdminCommand.DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
object IsRunning
|
||||
extends StatusAdminCommands.FromStatus({
|
||||
case v0.NodeStatus.Response.Empty => false
|
||||
case _ => true
|
||||
})
|
||||
|
||||
object IsInitialized
|
||||
extends StatusAdminCommands.FromStatus({
|
||||
case v0.NodeStatus.Response.Success(_) => true
|
||||
case _ => false
|
||||
})
|
||||
|
||||
class FromStatus(predicate: v0.NodeStatus.Response => Boolean) extends GetStatusBase[Boolean] {
|
||||
override def handleResponse(response: v0.NodeStatus): Either[String, Boolean] =
|
||||
(response.response match {
|
||||
case v0.NodeStatus.Response.Empty => Left(ProtoDeserializationError.FieldNotSet("response"))
|
||||
case other => Right(predicate(other))
|
||||
}).leftMap(_.toString)
|
||||
}
|
||||
}
|
@ -0,0 +1,774 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.daml.lf.data.Ref.PackageId
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
DefaultUnboundedTimeout,
|
||||
TimeoutType,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.data.*
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.crypto.{Fingerprint, KeyPurpose}
|
||||
import com.digitalasset.canton.protocol.{DynamicDomainParameters as DynamicDomainParametersInternal}
|
||||
import com.digitalasset.canton.topology.admin.grpc.BaseQuery
|
||||
import com.digitalasset.canton.topology.admin.v0
|
||||
import com.digitalasset.canton.topology.admin.v0.AuthorizationSuccess
|
||||
import com.digitalasset.canton.topology.admin.v0.InitializationServiceGrpc.InitializationServiceStub
|
||||
import com.digitalasset.canton.topology.admin.v0.TopologyAggregationServiceGrpc.TopologyAggregationServiceStub
|
||||
import com.digitalasset.canton.topology.admin.v0.TopologyManagerReadServiceGrpc.TopologyManagerReadServiceStub
|
||||
import com.digitalasset.canton.topology.admin.v0.TopologyManagerWriteServiceGrpc.TopologyManagerWriteServiceStub
|
||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactions
|
||||
import com.digitalasset.canton.topology.transaction.*
|
||||
import com.digitalasset.canton.topology.{DomainId, *}
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.empty.Empty
|
||||
import com.google.protobuf.timestamp.Timestamp
|
||||
import io.grpc.ManagedChannel
|
||||
|
||||
import java.time.Instant
|
||||
import scala.concurrent.Future
|
||||
|
||||
object TopologyAdminCommands {
|
||||
|
||||
object Aggregation {
|
||||
|
||||
abstract class BaseCommand[Req, Res, Result] extends GrpcAdminCommand[Req, Res, Result] {
|
||||
override type Svc = TopologyAggregationServiceStub
|
||||
override def createService(channel: ManagedChannel): TopologyAggregationServiceStub =
|
||||
v0.TopologyAggregationServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case class ListParties(
|
||||
filterDomain: String,
|
||||
filterParty: String,
|
||||
filterParticipant: String,
|
||||
asOf: Option[Instant],
|
||||
limit: PositiveInt,
|
||||
) extends BaseCommand[v0.ListPartiesRequest, v0.ListPartiesResponse, Seq[ListPartiesResult]] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListPartiesRequest] =
|
||||
Right(
|
||||
v0.ListPartiesRequest(
|
||||
filterDomain = filterDomain,
|
||||
filterParty = filterParty,
|
||||
filterParticipant = filterParticipant,
|
||||
asOf = asOf.map(ts => Timestamp(ts.getEpochSecond)),
|
||||
limit = limit.value,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyAggregationServiceStub,
|
||||
request: v0.ListPartiesRequest,
|
||||
): Future[v0.ListPartiesResponse] =
|
||||
service.listParties(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListPartiesResponse
|
||||
): Either[String, Seq[ListPartiesResult]] =
|
||||
response.results.traverse(ListPartiesResult.fromProtoV0).leftMap(_.toString)
|
||||
|
||||
// command will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
final case class ListKeyOwners(
|
||||
filterDomain: String,
|
||||
filterKeyOwnerType: Option[MemberCode],
|
||||
filterKeyOwnerUid: String,
|
||||
asOf: Option[Instant],
|
||||
limit: PositiveInt,
|
||||
) extends BaseCommand[v0.ListKeyOwnersRequest, v0.ListKeyOwnersResponse, Seq[
|
||||
ListKeyOwnersResult
|
||||
]] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListKeyOwnersRequest] =
|
||||
Right(
|
||||
v0.ListKeyOwnersRequest(
|
||||
filterDomain = filterDomain,
|
||||
filterKeyOwnerType = filterKeyOwnerType.map(_.toProtoPrimitive).getOrElse(""),
|
||||
filterKeyOwnerUid = filterKeyOwnerUid,
|
||||
asOf = asOf.map(ts => Timestamp(ts.getEpochSecond)),
|
||||
limit = limit.value,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyAggregationServiceStub,
|
||||
request: v0.ListKeyOwnersRequest,
|
||||
): Future[v0.ListKeyOwnersResponse] =
|
||||
service.listKeyOwners(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListKeyOwnersResponse
|
||||
): Either[String, Seq[ListKeyOwnersResult]] =
|
||||
response.results.traverse(ListKeyOwnersResult.fromProtoV0).leftMap(_.toString)
|
||||
|
||||
// command will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
object Write {
|
||||
|
||||
abstract class BaseWriteCommand[Req, Resp, Res] extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = TopologyManagerWriteServiceStub
|
||||
override def createService(channel: ManagedChannel): TopologyManagerWriteServiceStub =
|
||||
v0.TopologyManagerWriteServiceGrpc.stub(channel)
|
||||
|
||||
}
|
||||
|
||||
abstract class BaseCommand[Req]
|
||||
extends BaseWriteCommand[Req, v0.AuthorizationSuccess, ByteString] {
|
||||
|
||||
protected def authData(
|
||||
ops: TopologyChangeOp,
|
||||
signedBy: Option[Fingerprint],
|
||||
replaceExisting: Boolean,
|
||||
force: Boolean,
|
||||
) =
|
||||
Some(
|
||||
v0.AuthorizationData(
|
||||
ops.toProto,
|
||||
signedBy.map(_.unwrap).getOrElse(""),
|
||||
replaceExisting = replaceExisting,
|
||||
forceChange = force,
|
||||
)
|
||||
)
|
||||
|
||||
override def handleResponse(response: v0.AuthorizationSuccess): Either[String, ByteString] =
|
||||
Right(response.serialized)
|
||||
|
||||
}
|
||||
|
||||
final case class AuthorizeNamespaceDelegation(
|
||||
ops: TopologyChangeOp,
|
||||
signedBy: Option[Fingerprint],
|
||||
namespace: Fingerprint,
|
||||
authorizedKey: Fingerprint,
|
||||
isRootDelegation: Boolean,
|
||||
force: Boolean,
|
||||
) extends BaseCommand[v0.NamespaceDelegationAuthorization] {
|
||||
|
||||
override def createRequest(): Either[String, v0.NamespaceDelegationAuthorization] =
|
||||
Right(
|
||||
v0.NamespaceDelegationAuthorization(
|
||||
authData(ops, signedBy, replaceExisting = false, force = force),
|
||||
namespace.toProtoPrimitive,
|
||||
authorizedKey.toProtoPrimitive,
|
||||
isRootDelegation,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.NamespaceDelegationAuthorization,
|
||||
): Future[v0.AuthorizationSuccess] =
|
||||
service.authorizeNamespaceDelegation(request)
|
||||
|
||||
}
|
||||
|
||||
final case class AuthorizeIdentifierDelegation(
|
||||
ops: TopologyChangeOp,
|
||||
signedBy: Option[Fingerprint],
|
||||
identifier: UniqueIdentifier,
|
||||
authorizedKey: Fingerprint,
|
||||
) extends BaseCommand[v0.IdentifierDelegationAuthorization] {
|
||||
|
||||
override def createRequest(): Either[String, v0.IdentifierDelegationAuthorization] =
|
||||
Right(
|
||||
v0.IdentifierDelegationAuthorization(
|
||||
authData(ops, signedBy, replaceExisting = false, force = false),
|
||||
identifier.toProtoPrimitive,
|
||||
authorizedKey.toProtoPrimitive,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.IdentifierDelegationAuthorization,
|
||||
): Future[v0.AuthorizationSuccess] =
|
||||
service.authorizeIdentifierDelegation(request)
|
||||
|
||||
}
|
||||
|
||||
final case class AuthorizeOwnerToKeyMapping(
|
||||
ops: TopologyChangeOp,
|
||||
signedBy: Option[Fingerprint],
|
||||
keyOwner: Member,
|
||||
fingerprintOfKey: Fingerprint,
|
||||
purpose: KeyPurpose,
|
||||
force: Boolean,
|
||||
) extends BaseCommand[v0.OwnerToKeyMappingAuthorization] {
|
||||
|
||||
override def createRequest(): Either[String, v0.OwnerToKeyMappingAuthorization] = Right(
|
||||
v0.OwnerToKeyMappingAuthorization(
|
||||
authData(ops, signedBy, replaceExisting = false, force = force),
|
||||
keyOwner.toProtoPrimitive,
|
||||
fingerprintOfKey.toProtoPrimitive,
|
||||
purpose.toProtoEnum,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.OwnerToKeyMappingAuthorization,
|
||||
): Future[v0.AuthorizationSuccess] =
|
||||
service.authorizeOwnerToKeyMapping(request)
|
||||
|
||||
}
|
||||
|
||||
final case class AuthorizePartyToParticipant(
|
||||
ops: TopologyChangeOp,
|
||||
signedBy: Option[Fingerprint],
|
||||
side: RequestSide,
|
||||
party: PartyId,
|
||||
participant: ParticipantId,
|
||||
permission: ParticipantPermission,
|
||||
replaceExisting: Boolean,
|
||||
force: Boolean,
|
||||
) extends BaseCommand[v0.PartyToParticipantAuthorization] {
|
||||
|
||||
override def createRequest(): Either[String, v0.PartyToParticipantAuthorization] =
|
||||
Right(
|
||||
v0.PartyToParticipantAuthorization(
|
||||
authData(ops, signedBy, replaceExisting = replaceExisting, force = force),
|
||||
side.toProtoEnum,
|
||||
party.uid.toProtoPrimitive,
|
||||
participant.toProtoPrimitive,
|
||||
permission.toProtoEnum,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.PartyToParticipantAuthorization,
|
||||
): Future[v0.AuthorizationSuccess] =
|
||||
service.authorizePartyToParticipant(request)
|
||||
|
||||
}
|
||||
|
||||
final case class AuthorizeParticipantDomainState(
|
||||
ops: TopologyChangeOp,
|
||||
signedBy: Option[Fingerprint],
|
||||
side: RequestSide,
|
||||
domain: DomainId,
|
||||
participant: ParticipantId,
|
||||
permission: ParticipantPermission,
|
||||
trustLevel: TrustLevel,
|
||||
replaceExisting: Boolean,
|
||||
) extends BaseCommand[v0.ParticipantDomainStateAuthorization] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ParticipantDomainStateAuthorization] =
|
||||
Right(
|
||||
v0.ParticipantDomainStateAuthorization(
|
||||
authData(ops, signedBy, replaceExisting = replaceExisting, force = false),
|
||||
side.toProtoEnum,
|
||||
domain.unwrap.toProtoPrimitive,
|
||||
participant.toProtoPrimitive,
|
||||
permission.toProtoEnum,
|
||||
trustLevel.toProtoEnum,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.ParticipantDomainStateAuthorization,
|
||||
): Future[v0.AuthorizationSuccess] =
|
||||
service.authorizeParticipantDomainState(request)
|
||||
|
||||
}
|
||||
|
||||
final case class AuthorizeMediatorDomainState(
|
||||
ops: TopologyChangeOp,
|
||||
signedBy: Option[Fingerprint],
|
||||
side: RequestSide,
|
||||
domain: DomainId,
|
||||
mediator: MediatorId,
|
||||
replaceExisting: Boolean,
|
||||
) extends BaseCommand[v0.MediatorDomainStateAuthorization] {
|
||||
|
||||
override def createRequest(): Either[String, v0.MediatorDomainStateAuthorization] =
|
||||
Right(
|
||||
v0.MediatorDomainStateAuthorization(
|
||||
authData(ops, signedBy, replaceExisting = replaceExisting, force = false),
|
||||
side.toProtoEnum,
|
||||
domain.unwrap.toProtoPrimitive,
|
||||
mediator.uid.toProtoPrimitive,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.MediatorDomainStateAuthorization,
|
||||
): Future[v0.AuthorizationSuccess] =
|
||||
service.authorizeMediatorDomainState(request)
|
||||
|
||||
}
|
||||
|
||||
final case class AuthorizeVettedPackages(
|
||||
ops: TopologyChangeOp,
|
||||
signedBy: Option[Fingerprint],
|
||||
participant: ParticipantId,
|
||||
packageIds: Seq[PackageId],
|
||||
force: Boolean,
|
||||
) extends BaseCommand[v0.VettedPackagesAuthorization] {
|
||||
|
||||
override def createRequest(): Either[String, v0.VettedPackagesAuthorization] =
|
||||
Right(
|
||||
v0.VettedPackagesAuthorization(
|
||||
authData(ops, signedBy, replaceExisting = false, force = force),
|
||||
participant.uid.toProtoPrimitive,
|
||||
packageIds = packageIds,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.VettedPackagesAuthorization,
|
||||
): Future[v0.AuthorizationSuccess] =
|
||||
service.authorizeVettedPackages(request)
|
||||
|
||||
}
|
||||
|
||||
final case class AuthorizeDomainParametersChange(
|
||||
signedBy: Option[Fingerprint],
|
||||
domainId: DomainId,
|
||||
newParameters: DynamicDomainParameters,
|
||||
force: Boolean,
|
||||
) extends BaseCommand[v0.DomainParametersChangeAuthorization] {
|
||||
override def createRequest(): Either[String, v0.DomainParametersChangeAuthorization] =
|
||||
v0.DomainParametersChangeAuthorization(
|
||||
authorization =
|
||||
authData(TopologyChangeOp.Replace, signedBy, replaceExisting = false, force = force),
|
||||
domain = domainId.toProtoPrimitive,
|
||||
parameters = newParameters.toProto,
|
||||
).asRight
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.DomainParametersChangeAuthorization,
|
||||
): Future[AuthorizationSuccess] = service.authorizeDomainParametersChange(request)
|
||||
}
|
||||
|
||||
final case class AuthorizeDomainParametersChangeInternal(
|
||||
signedBy: Option[Fingerprint],
|
||||
domainId: DomainId,
|
||||
newParameters: DynamicDomainParametersInternal,
|
||||
force: Boolean,
|
||||
) extends BaseCommand[v0.DomainParametersChangeAuthorization] {
|
||||
override def createRequest(): Either[String, v0.DomainParametersChangeAuthorization] = {
|
||||
val parameters =
|
||||
v0.DomainParametersChangeAuthorization.Parameters
|
||||
.ParametersV1(newParameters.toProtoV2)
|
||||
|
||||
v0.DomainParametersChangeAuthorization(
|
||||
authorization =
|
||||
authData(TopologyChangeOp.Replace, signedBy, replaceExisting = false, force = force),
|
||||
domain = domainId.toProtoPrimitive,
|
||||
parameters = parameters,
|
||||
).asRight
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.DomainParametersChangeAuthorization,
|
||||
): Future[AuthorizationSuccess] = service.authorizeDomainParametersChange(request)
|
||||
}
|
||||
|
||||
final case class AddSignedTopologyTransaction(bytes: ByteString)
|
||||
extends BaseWriteCommand[v0.SignedTopologyTransactionAddition, v0.AdditionSuccess, Unit] {
|
||||
|
||||
override def createRequest(): Either[String, v0.SignedTopologyTransactionAddition] =
|
||||
Right(v0.SignedTopologyTransactionAddition(serialized = bytes))
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceStub,
|
||||
request: v0.SignedTopologyTransactionAddition,
|
||||
): Future[v0.AdditionSuccess] =
|
||||
service.addSignedTopologyTransaction(request)
|
||||
|
||||
override def handleResponse(response: v0.AdditionSuccess): Either[String, Unit] =
|
||||
Right(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Read {
|
||||
|
||||
abstract class BaseCommand[Req, Res, Ret] extends GrpcAdminCommand[Req, Res, Ret] {
|
||||
override type Svc = TopologyManagerReadServiceStub
|
||||
override def createService(channel: ManagedChannel): TopologyManagerReadServiceStub =
|
||||
v0.TopologyManagerReadServiceGrpc.stub(channel)
|
||||
|
||||
// command will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
final case class ListPartyToParticipant(
|
||||
query: BaseQuery,
|
||||
filterParty: String,
|
||||
filterParticipant: String,
|
||||
filterRequestSide: Option[RequestSide],
|
||||
filterPermission: Option[ParticipantPermission],
|
||||
) extends BaseCommand[v0.ListPartyToParticipantRequest, v0.ListPartyToParticipantResult, Seq[
|
||||
ListPartyToParticipantResult
|
||||
]] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListPartyToParticipantRequest] =
|
||||
Right(
|
||||
new v0.ListPartyToParticipantRequest(
|
||||
baseQuery = Some(query.toProtoV0),
|
||||
filterParty,
|
||||
filterParticipant,
|
||||
filterRequestSide
|
||||
.map(_.toProtoEnum)
|
||||
.map(new v0.ListPartyToParticipantRequest.FilterRequestSide(_)),
|
||||
filterPermission
|
||||
.map(_.toProtoEnum)
|
||||
.map(new v0.ListPartyToParticipantRequest.FilterPermission(_)),
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListPartyToParticipantRequest,
|
||||
): Future[v0.ListPartyToParticipantResult] =
|
||||
service.listPartyToParticipant(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListPartyToParticipantResult
|
||||
): Either[String, Seq[ListPartyToParticipantResult]] =
|
||||
response.results.traverse(ListPartyToParticipantResult.fromProtoV0).leftMap(_.toString)
|
||||
|
||||
}
|
||||
|
||||
final case class ListOwnerToKeyMapping(
|
||||
query: BaseQuery,
|
||||
filterKeyOwnerType: Option[MemberCode],
|
||||
filterKeyOwnerUid: String,
|
||||
filterKeyPurpose: Option[KeyPurpose],
|
||||
) extends BaseCommand[v0.ListOwnerToKeyMappingRequest, v0.ListOwnerToKeyMappingResult, Seq[
|
||||
ListOwnerToKeyMappingResult
|
||||
]] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListOwnerToKeyMappingRequest] =
|
||||
Right(
|
||||
new v0.ListOwnerToKeyMappingRequest(
|
||||
baseQuery = Some(query.toProtoV0),
|
||||
filterKeyOwnerType = filterKeyOwnerType.map(_.toProtoPrimitive).getOrElse(""),
|
||||
filterKeyOwnerUid = filterKeyOwnerUid,
|
||||
filterKeyPurpose
|
||||
.map(_.toProtoEnum)
|
||||
.map(new admin.v0.ListOwnerToKeyMappingRequest.FilterKeyPurpose(_)),
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListOwnerToKeyMappingRequest,
|
||||
): Future[v0.ListOwnerToKeyMappingResult] =
|
||||
service.listOwnerToKeyMapping(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListOwnerToKeyMappingResult
|
||||
): Either[String, Seq[ListOwnerToKeyMappingResult]] =
|
||||
response.results.traverse(ListOwnerToKeyMappingResult.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListNamespaceDelegation(query: BaseQuery, filterNamespace: String)
|
||||
extends BaseCommand[
|
||||
v0.ListNamespaceDelegationRequest,
|
||||
v0.ListNamespaceDelegationResult,
|
||||
Seq[ListNamespaceDelegationResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListNamespaceDelegationRequest] =
|
||||
Right(
|
||||
new v0.ListNamespaceDelegationRequest(
|
||||
baseQuery = Some(query.toProtoV0),
|
||||
filterNamespace = filterNamespace,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListNamespaceDelegationRequest,
|
||||
): Future[v0.ListNamespaceDelegationResult] =
|
||||
service.listNamespaceDelegation(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListNamespaceDelegationResult
|
||||
): Either[String, Seq[ListNamespaceDelegationResult]] =
|
||||
response.results.traverse(ListNamespaceDelegationResult.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListIdentifierDelegation(query: BaseQuery, filterUid: String)
|
||||
extends BaseCommand[
|
||||
v0.ListIdentifierDelegationRequest,
|
||||
v0.ListIdentifierDelegationResult,
|
||||
Seq[ListIdentifierDelegationResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListIdentifierDelegationRequest] =
|
||||
Right(
|
||||
new v0.ListIdentifierDelegationRequest(
|
||||
baseQuery = Some(query.toProtoV0),
|
||||
filterUid = filterUid,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListIdentifierDelegationRequest,
|
||||
): Future[v0.ListIdentifierDelegationResult] =
|
||||
service.listIdentifierDelegation(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListIdentifierDelegationResult
|
||||
): Either[String, Seq[ListIdentifierDelegationResult]] =
|
||||
response.results.traverse(ListIdentifierDelegationResult.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListSignedLegalIdentityClaim(query: BaseQuery, filterUid: String)
|
||||
extends BaseCommand[
|
||||
v0.ListSignedLegalIdentityClaimRequest,
|
||||
v0.ListSignedLegalIdentityClaimResult,
|
||||
Seq[ListSignedLegalIdentityClaimResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListSignedLegalIdentityClaimRequest] =
|
||||
Right(
|
||||
new v0.ListSignedLegalIdentityClaimRequest(
|
||||
baseQuery = Some(query.toProtoV0),
|
||||
filterUid = filterUid,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListSignedLegalIdentityClaimRequest,
|
||||
): Future[v0.ListSignedLegalIdentityClaimResult] =
|
||||
service.listSignedLegalIdentityClaim(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListSignedLegalIdentityClaimResult
|
||||
): Either[String, Seq[ListSignedLegalIdentityClaimResult]] =
|
||||
response.results
|
||||
.traverse(ListSignedLegalIdentityClaimResult.fromProtoV0)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListVettedPackages(query: BaseQuery, filterParticipant: String)
|
||||
extends BaseCommand[v0.ListVettedPackagesRequest, v0.ListVettedPackagesResult, Seq[
|
||||
ListVettedPackagesResult
|
||||
]] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListVettedPackagesRequest] =
|
||||
Right(
|
||||
new v0.ListVettedPackagesRequest(
|
||||
baseQuery = Some(query.toProtoV0),
|
||||
filterParticipant,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListVettedPackagesRequest,
|
||||
): Future[v0.ListVettedPackagesResult] =
|
||||
service.listVettedPackages(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListVettedPackagesResult
|
||||
): Either[String, Seq[ListVettedPackagesResult]] =
|
||||
response.results.traverse(ListVettedPackagesResult.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListDomainParametersChanges(query: BaseQuery)
|
||||
extends BaseCommand[
|
||||
v0.ListDomainParametersChangesRequest,
|
||||
v0.ListDomainParametersChangesResult,
|
||||
Seq[ListDomainParametersChangeResult],
|
||||
] {
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListDomainParametersChangesRequest,
|
||||
): Future[v0.ListDomainParametersChangesResult] = service.listDomainParametersChanges(request)
|
||||
|
||||
override def createRequest(): Either[String, v0.ListDomainParametersChangesRequest] = Right(
|
||||
v0.ListDomainParametersChangesRequest(Some(query.toProtoV0))
|
||||
)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListDomainParametersChangesResult
|
||||
): Either[String, Seq[ListDomainParametersChangeResult]] =
|
||||
response.results.traverse(ListDomainParametersChangeResult.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListStores()
|
||||
extends BaseCommand[v0.ListAvailableStoresRequest, v0.ListAvailableStoresResult, Seq[
|
||||
String
|
||||
]] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListAvailableStoresRequest] =
|
||||
Right(v0.ListAvailableStoresRequest())
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListAvailableStoresRequest,
|
||||
): Future[v0.ListAvailableStoresResult] =
|
||||
service.listAvailableStores(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListAvailableStoresResult
|
||||
): Either[String, Seq[String]] =
|
||||
Right(response.storeIds)
|
||||
}
|
||||
|
||||
final case class ListParticipantDomainState(
|
||||
query: BaseQuery,
|
||||
filterDomain: String,
|
||||
filterParticipant: String,
|
||||
) extends BaseCommand[
|
||||
v0.ListParticipantDomainStateRequest,
|
||||
v0.ListParticipantDomainStateResult,
|
||||
Seq[ListParticipantDomainStateResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListParticipantDomainStateRequest] =
|
||||
Right(
|
||||
new v0.ListParticipantDomainStateRequest(
|
||||
baseQuery = Some(query.toProtoV0),
|
||||
filterDomain = filterDomain,
|
||||
filterParticipant = filterParticipant,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListParticipantDomainStateRequest,
|
||||
): Future[v0.ListParticipantDomainStateResult] =
|
||||
service.listParticipantDomainState(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListParticipantDomainStateResult
|
||||
): Either[String, Seq[ListParticipantDomainStateResult]] =
|
||||
response.results.traverse(ListParticipantDomainStateResult.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListMediatorDomainState(
|
||||
query: BaseQuery,
|
||||
filterDomain: String,
|
||||
filterMediator: String,
|
||||
) extends BaseCommand[
|
||||
v0.ListMediatorDomainStateRequest,
|
||||
v0.ListMediatorDomainStateResult,
|
||||
Seq[ListMediatorDomainStateResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListMediatorDomainStateRequest] =
|
||||
Right(
|
||||
new v0.ListMediatorDomainStateRequest(
|
||||
baseQuery = Some(query.toProtoV0),
|
||||
filterDomain = filterDomain,
|
||||
filterMediator = filterMediator,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListMediatorDomainStateRequest,
|
||||
): Future[v0.ListMediatorDomainStateResult] =
|
||||
service.listMediatorDomainState(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListMediatorDomainStateResult
|
||||
): Either[String, Seq[ListMediatorDomainStateResult]] =
|
||||
response.results.traverse(ListMediatorDomainStateResult.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListAll(query: BaseQuery)
|
||||
extends BaseCommand[
|
||||
v0.ListAllRequest,
|
||||
v0.ListAllResponse,
|
||||
StoredTopologyTransactions[
|
||||
TopologyChangeOp
|
||||
],
|
||||
] {
|
||||
override def createRequest(): Either[String, v0.ListAllRequest] =
|
||||
Right(new v0.ListAllRequest(Some(query.toProtoV0)))
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceStub,
|
||||
request: v0.ListAllRequest,
|
||||
): Future[v0.ListAllResponse] = service.listAll(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListAllResponse
|
||||
): Either[String, StoredTopologyTransactions[TopologyChangeOp]] =
|
||||
response.result
|
||||
.fold[Either[String, StoredTopologyTransactions[TopologyChangeOp]]](
|
||||
Right(StoredTopologyTransactions.empty)
|
||||
) { collection =>
|
||||
StoredTopologyTransactions.fromProtoV0(collection).leftMap(_.toString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Init {
|
||||
|
||||
abstract class BaseInitializationService[Req, Resp, Res]
|
||||
extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = InitializationServiceStub
|
||||
override def createService(channel: ManagedChannel): InitializationServiceStub =
|
||||
v0.InitializationServiceGrpc.stub(channel)
|
||||
|
||||
}
|
||||
|
||||
final case class InitId(identifier: String, fingerprint: String)
|
||||
extends BaseInitializationService[v0.InitIdRequest, v0.InitIdResponse, UniqueIdentifier] {
|
||||
|
||||
override def createRequest(): Either[String, v0.InitIdRequest] =
|
||||
Right(v0.InitIdRequest(identifier, fingerprint, instance = ""))
|
||||
|
||||
override def submitRequest(
|
||||
service: InitializationServiceStub,
|
||||
request: v0.InitIdRequest,
|
||||
): Future[v0.InitIdResponse] =
|
||||
service.initId(request)
|
||||
|
||||
override def handleResponse(response: v0.InitIdResponse): Either[String, UniqueIdentifier] =
|
||||
UniqueIdentifier.fromProtoPrimitive_(response.uniqueIdentifier)
|
||||
}
|
||||
|
||||
final case class GetId()
|
||||
extends BaseInitializationService[Empty, v0.GetIdResponse, UniqueIdentifier] {
|
||||
override def createRequest(): Either[String, Empty] =
|
||||
Right(Empty())
|
||||
|
||||
override def submitRequest(
|
||||
service: InitializationServiceStub,
|
||||
request: Empty,
|
||||
): Future[v0.GetIdResponse] =
|
||||
service.getId(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.GetIdResponse
|
||||
): Either[String, UniqueIdentifier] = {
|
||||
if (response.uniqueIdentifier.nonEmpty)
|
||||
UniqueIdentifier.fromProtoPrimitive_(response.uniqueIdentifier)
|
||||
else
|
||||
Left(
|
||||
s"Node ${response.instance} is not initialized and therefore does not have an Id assigned yet."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,771 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
DefaultUnboundedTimeout,
|
||||
TimeoutType,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.data.topologyx.*
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.crypto.Fingerprint
|
||||
import com.digitalasset.canton.topology.*
|
||||
import com.digitalasset.canton.topology.admin.grpc.BaseQueryX
|
||||
import com.digitalasset.canton.topology.admin.v1
|
||||
import com.digitalasset.canton.topology.admin.v1.AuthorizeRequest.Type.{Proposal, TransactionHash}
|
||||
import com.digitalasset.canton.topology.admin.v1.IdentityInitializationServiceXGrpc.IdentityInitializationServiceXStub
|
||||
import com.digitalasset.canton.topology.admin.v1.TopologyManagerReadServiceXGrpc.TopologyManagerReadServiceXStub
|
||||
import com.digitalasset.canton.topology.admin.v1.TopologyManagerWriteServiceXGrpc.TopologyManagerWriteServiceXStub
|
||||
import com.digitalasset.canton.topology.admin.v1.{
|
||||
AddTransactionsRequest,
|
||||
AddTransactionsResponse,
|
||||
AuthorizeRequest,
|
||||
AuthorizeResponse,
|
||||
ListTrafficStateRequest,
|
||||
SignTransactionsRequest,
|
||||
SignTransactionsResponse,
|
||||
}
|
||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX
|
||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactionsX.GenericStoredTopologyTransactionsX
|
||||
import com.digitalasset.canton.topology.transaction.SignedTopologyTransactionX.GenericSignedTopologyTransactionX
|
||||
import com.digitalasset.canton.topology.transaction.{
|
||||
SignedTopologyTransactionX,
|
||||
TopologyChangeOpX,
|
||||
TopologyMappingX,
|
||||
}
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ManagedChannel
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
object TopologyAdminCommandsX {
|
||||
|
||||
object Read {
|
||||
|
||||
abstract class BaseCommand[Req, Res, Ret] extends GrpcAdminCommand[Req, Res, Ret] {
|
||||
override type Svc = TopologyManagerReadServiceXStub
|
||||
override def createService(channel: ManagedChannel): TopologyManagerReadServiceXStub =
|
||||
v1.TopologyManagerReadServiceXGrpc.stub(channel)
|
||||
|
||||
// command will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
final case class ListTrafficControlState(
|
||||
query: BaseQueryX,
|
||||
filterMember: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListTrafficStateRequest,
|
||||
v1.ListTrafficStateResult,
|
||||
Seq[ListTrafficStateResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListTrafficStateRequest] =
|
||||
Right(
|
||||
new ListTrafficStateRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterMember = filterMember,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListTrafficStateRequest,
|
||||
): Future[v1.ListTrafficStateResult] =
|
||||
service.listTrafficState(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListTrafficStateResult
|
||||
): Either[String, Seq[ListTrafficStateResult]] =
|
||||
response.results
|
||||
.traverse(ListTrafficStateResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListNamespaceDelegation(
|
||||
query: BaseQueryX,
|
||||
filterNamespace: String,
|
||||
filterTargetKey: Option[Fingerprint],
|
||||
) extends BaseCommand[
|
||||
v1.ListNamespaceDelegationRequest,
|
||||
v1.ListNamespaceDelegationResult,
|
||||
Seq[ListNamespaceDelegationResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListNamespaceDelegationRequest] =
|
||||
Right(
|
||||
new v1.ListNamespaceDelegationRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterNamespace = filterNamespace,
|
||||
filterTargetKeyFingerprint = filterTargetKey.map(_.toProtoPrimitive).getOrElse(""),
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListNamespaceDelegationRequest,
|
||||
): Future[v1.ListNamespaceDelegationResult] =
|
||||
service.listNamespaceDelegation(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListNamespaceDelegationResult
|
||||
): Either[String, Seq[ListNamespaceDelegationResult]] =
|
||||
response.results.traverse(ListNamespaceDelegationResult.fromProtoV1).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListUnionspaceDefinition(
|
||||
query: BaseQueryX,
|
||||
filterNamespace: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListUnionspaceDefinitionRequest,
|
||||
v1.ListUnionspaceDefinitionResult,
|
||||
Seq[ListUnionspaceDefinitionResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListUnionspaceDefinitionRequest] =
|
||||
Right(
|
||||
new v1.ListUnionspaceDefinitionRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterNamespace = filterNamespace,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListUnionspaceDefinitionRequest,
|
||||
): Future[v1.ListUnionspaceDefinitionResult] =
|
||||
service.listUnionspaceDefinition(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListUnionspaceDefinitionResult
|
||||
): Either[String, Seq[ListUnionspaceDefinitionResult]] =
|
||||
response.results.traverse(ListUnionspaceDefinitionResult.fromProtoV1).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListIdentifierDelegation(
|
||||
query: BaseQueryX,
|
||||
filterUid: String,
|
||||
filterTargetKey: Option[Fingerprint],
|
||||
) extends BaseCommand[
|
||||
v1.ListIdentifierDelegationRequest,
|
||||
v1.ListIdentifierDelegationResult,
|
||||
Seq[ListIdentifierDelegationResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListIdentifierDelegationRequest] =
|
||||
Right(
|
||||
new v1.ListIdentifierDelegationRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterUid = filterUid,
|
||||
filterTargetKeyFingerprint = filterTargetKey.map(_.toProtoPrimitive).getOrElse(""),
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListIdentifierDelegationRequest,
|
||||
): Future[v1.ListIdentifierDelegationResult] =
|
||||
service.listIdentifierDelegation(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListIdentifierDelegationResult
|
||||
): Either[String, Seq[ListIdentifierDelegationResult]] =
|
||||
response.results.traverse(ListIdentifierDelegationResult.fromProtoV1).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListOwnerToKeyMapping(
|
||||
query: BaseQueryX,
|
||||
filterKeyOwnerType: Option[MemberCode],
|
||||
filterKeyOwnerUid: String,
|
||||
) extends BaseCommand[v1.ListOwnerToKeyMappingRequest, v1.ListOwnerToKeyMappingResult, Seq[
|
||||
ListOwnerToKeyMappingResult
|
||||
]] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListOwnerToKeyMappingRequest] =
|
||||
Right(
|
||||
new v1.ListOwnerToKeyMappingRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterKeyOwnerType = filterKeyOwnerType.map(_.toProtoPrimitive).getOrElse(""),
|
||||
filterKeyOwnerUid = filterKeyOwnerUid,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListOwnerToKeyMappingRequest,
|
||||
): Future[v1.ListOwnerToKeyMappingResult] =
|
||||
service.listOwnerToKeyMapping(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListOwnerToKeyMappingResult
|
||||
): Either[String, Seq[ListOwnerToKeyMappingResult]] =
|
||||
response.results.traverse(ListOwnerToKeyMappingResult.fromProtoV1).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListDomainTrustCertificate(
|
||||
query: BaseQueryX,
|
||||
filterUid: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListDomainTrustCertificateRequest,
|
||||
v1.ListDomainTrustCertificateResult,
|
||||
Seq[ListDomainTrustCertificateResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListDomainTrustCertificateRequest] =
|
||||
Right(
|
||||
new v1.ListDomainTrustCertificateRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterUid = filterUid,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListDomainTrustCertificateRequest,
|
||||
): Future[v1.ListDomainTrustCertificateResult] =
|
||||
service.listDomainTrustCertificate(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListDomainTrustCertificateResult
|
||||
): Either[String, Seq[ListDomainTrustCertificateResult]] =
|
||||
response.results.traverse(ListDomainTrustCertificateResult.fromProtoV1).leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListParticipantDomainPermission(
|
||||
query: BaseQueryX,
|
||||
filterUid: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListParticipantDomainPermissionRequest,
|
||||
v1.ListParticipantDomainPermissionResult,
|
||||
Seq[ListParticipantDomainPermissionResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListParticipantDomainPermissionRequest] =
|
||||
Right(
|
||||
new v1.ListParticipantDomainPermissionRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterUid = filterUid,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListParticipantDomainPermissionRequest,
|
||||
): Future[v1.ListParticipantDomainPermissionResult] =
|
||||
service.listParticipantDomainPermission(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListParticipantDomainPermissionResult
|
||||
): Either[String, Seq[ListParticipantDomainPermissionResult]] =
|
||||
response.results
|
||||
.traverse(ListParticipantDomainPermissionResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListPartyHostingLimits(
|
||||
query: BaseQueryX,
|
||||
filterUid: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListPartyHostingLimitsRequest,
|
||||
v1.ListPartyHostingLimitsResult,
|
||||
Seq[ListPartyHostingLimitsResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListPartyHostingLimitsRequest] =
|
||||
Right(
|
||||
new v1.ListPartyHostingLimitsRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterUid = filterUid,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListPartyHostingLimitsRequest,
|
||||
): Future[v1.ListPartyHostingLimitsResult] =
|
||||
service.listPartyHostingLimits(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListPartyHostingLimitsResult
|
||||
): Either[String, Seq[ListPartyHostingLimitsResult]] =
|
||||
response.results
|
||||
.traverse(ListPartyHostingLimitsResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListVettedPackages(
|
||||
query: BaseQueryX,
|
||||
filterParticipant: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListVettedPackagesRequest,
|
||||
v1.ListVettedPackagesResult,
|
||||
Seq[ListVettedPackagesResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListVettedPackagesRequest] =
|
||||
Right(
|
||||
new v1.ListVettedPackagesRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterParticipant = filterParticipant,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListVettedPackagesRequest,
|
||||
): Future[v1.ListVettedPackagesResult] =
|
||||
service.listVettedPackages(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListVettedPackagesResult
|
||||
): Either[String, Seq[ListVettedPackagesResult]] =
|
||||
response.results
|
||||
.traverse(ListVettedPackagesResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListPartyToParticipant(
|
||||
query: BaseQueryX,
|
||||
filterParty: String,
|
||||
filterParticipant: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListPartyToParticipantRequest,
|
||||
v1.ListPartyToParticipantResult,
|
||||
Seq[ListPartyToParticipantResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListPartyToParticipantRequest] =
|
||||
Right(
|
||||
new v1.ListPartyToParticipantRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterParty = filterParty,
|
||||
filterParticipant = filterParticipant,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListPartyToParticipantRequest,
|
||||
): Future[v1.ListPartyToParticipantResult] =
|
||||
service.listPartyToParticipant(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListPartyToParticipantResult
|
||||
): Either[String, Seq[ListPartyToParticipantResult]] =
|
||||
response.results
|
||||
.traverse(ListPartyToParticipantResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListAuthorityOf(
|
||||
query: BaseQueryX,
|
||||
filterParty: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListAuthorityOfRequest,
|
||||
v1.ListAuthorityOfResult,
|
||||
Seq[ListAuthorityOfResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListAuthorityOfRequest] =
|
||||
Right(
|
||||
new v1.ListAuthorityOfRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterParty = filterParty,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListAuthorityOfRequest,
|
||||
): Future[v1.ListAuthorityOfResult] =
|
||||
service.listAuthorityOf(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListAuthorityOfResult
|
||||
): Either[String, Seq[ListAuthorityOfResult]] =
|
||||
response.results
|
||||
.traverse(ListAuthorityOfResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class DomainParametersState(
|
||||
query: BaseQueryX,
|
||||
filterDomain: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListDomainParametersStateRequest,
|
||||
v1.ListDomainParametersStateResult,
|
||||
Seq[ListDomainParametersStateResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListDomainParametersStateRequest] =
|
||||
Right(
|
||||
new v1.ListDomainParametersStateRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterDomain = filterDomain,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListDomainParametersStateRequest,
|
||||
): Future[v1.ListDomainParametersStateResult] =
|
||||
service.listDomainParametersState(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListDomainParametersStateResult
|
||||
): Either[String, Seq[ListDomainParametersStateResult]] =
|
||||
response.results
|
||||
.traverse(ListDomainParametersStateResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class MediatorDomainState(
|
||||
query: BaseQueryX,
|
||||
filterDomain: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListMediatorDomainStateRequest,
|
||||
v1.ListMediatorDomainStateResult,
|
||||
Seq[ListMediatorDomainStateResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListMediatorDomainStateRequest] =
|
||||
Right(
|
||||
new v1.ListMediatorDomainStateRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterDomain = filterDomain,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListMediatorDomainStateRequest,
|
||||
): Future[v1.ListMediatorDomainStateResult] =
|
||||
service.listMediatorDomainState(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListMediatorDomainStateResult
|
||||
): Either[String, Seq[ListMediatorDomainStateResult]] =
|
||||
response.results
|
||||
.traverse(ListMediatorDomainStateResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class SequencerDomainState(
|
||||
query: BaseQueryX,
|
||||
filterDomain: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListSequencerDomainStateRequest,
|
||||
v1.ListSequencerDomainStateResult,
|
||||
Seq[ListSequencerDomainStateResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListSequencerDomainStateRequest] =
|
||||
Right(
|
||||
new v1.ListSequencerDomainStateRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterDomain = filterDomain,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListSequencerDomainStateRequest,
|
||||
): Future[v1.ListSequencerDomainStateResult] =
|
||||
service.listSequencerDomainState(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListSequencerDomainStateResult
|
||||
): Either[String, Seq[ListSequencerDomainStateResult]] =
|
||||
response.results
|
||||
.traverse(ListSequencerDomainStateResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class PurgeTopologyTransactionX(
|
||||
query: BaseQueryX,
|
||||
filterDomain: String,
|
||||
) extends BaseCommand[
|
||||
v1.ListPurgeTopologyTransactionXRequest,
|
||||
v1.ListPurgeTopologyTransactionXResult,
|
||||
Seq[ListPurgeTopologyTransactionXResult],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListPurgeTopologyTransactionXRequest] =
|
||||
Right(
|
||||
new v1.ListPurgeTopologyTransactionXRequest(
|
||||
baseQuery = Some(query.toProtoV1),
|
||||
filterDomain = filterDomain,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListPurgeTopologyTransactionXRequest,
|
||||
): Future[v1.ListPurgeTopologyTransactionXResult] =
|
||||
service.listPurgeTopologyTransactionX(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListPurgeTopologyTransactionXResult
|
||||
): Either[String, Seq[ListPurgeTopologyTransactionXResult]] =
|
||||
response.results
|
||||
.traverse(ListPurgeTopologyTransactionXResult.fromProtoV1)
|
||||
.leftMap(_.toString)
|
||||
}
|
||||
|
||||
final case class ListStores()
|
||||
extends BaseCommand[v1.ListAvailableStoresRequest, v1.ListAvailableStoresResult, Seq[
|
||||
String
|
||||
]] {
|
||||
|
||||
override def createRequest(): Either[String, v1.ListAvailableStoresRequest] =
|
||||
Right(v1.ListAvailableStoresRequest())
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListAvailableStoresRequest,
|
||||
): Future[v1.ListAvailableStoresResult] =
|
||||
service.listAvailableStores(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListAvailableStoresResult
|
||||
): Either[String, Seq[String]] =
|
||||
Right(response.storeIds)
|
||||
}
|
||||
|
||||
final case class ListAll(query: BaseQueryX)
|
||||
extends BaseCommand[
|
||||
v1.ListAllRequest,
|
||||
v1.ListAllResponse,
|
||||
GenericStoredTopologyTransactionsX,
|
||||
] {
|
||||
override def createRequest(): Either[String, v1.ListAllRequest] =
|
||||
Right(new v1.ListAllRequest(Some(query.toProtoV1)))
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerReadServiceXStub,
|
||||
request: v1.ListAllRequest,
|
||||
): Future[v1.ListAllResponse] = service.listAll(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.ListAllResponse
|
||||
): Either[String, GenericStoredTopologyTransactionsX] =
|
||||
response.result
|
||||
.fold[Either[String, GenericStoredTopologyTransactionsX]](
|
||||
Right(StoredTopologyTransactionsX.empty)
|
||||
) { collection =>
|
||||
StoredTopologyTransactionsX.fromProtoV0(collection).leftMap(_.toString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Write {
|
||||
abstract class BaseWriteCommand[Req, Res, Ret] extends GrpcAdminCommand[Req, Res, Ret] {
|
||||
override type Svc = TopologyManagerWriteServiceXStub
|
||||
|
||||
override def createService(channel: ManagedChannel): TopologyManagerWriteServiceXStub =
|
||||
v1.TopologyManagerWriteServiceXGrpc.stub(channel)
|
||||
|
||||
// command will potentially take a long time
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
}
|
||||
|
||||
final case class AddTransactions(
|
||||
transactions: Seq[GenericSignedTopologyTransactionX],
|
||||
store: String,
|
||||
) extends BaseWriteCommand[AddTransactionsRequest, AddTransactionsResponse, Unit] {
|
||||
override def createRequest(): Either[String, AddTransactionsRequest] = {
|
||||
Right(AddTransactionsRequest(transactions.map(_.toProtoV2), forceChange = false, store))
|
||||
}
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceXStub,
|
||||
request: AddTransactionsRequest,
|
||||
): Future[AddTransactionsResponse] = service.addTransactions(request)
|
||||
override def handleResponse(response: AddTransactionsResponse): Either[String, Unit] =
|
||||
Right(())
|
||||
}
|
||||
|
||||
final case class SignTransactions(
|
||||
transactions: Seq[GenericSignedTopologyTransactionX],
|
||||
signedBy: Seq[Fingerprint],
|
||||
) extends BaseWriteCommand[SignTransactionsRequest, SignTransactionsResponse, Seq[
|
||||
GenericSignedTopologyTransactionX
|
||||
]] {
|
||||
override def createRequest(): Either[String, SignTransactionsRequest] = {
|
||||
Right(
|
||||
SignTransactionsRequest(transactions.map(_.toProtoV2), signedBy.map(_.toProtoPrimitive))
|
||||
)
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceXStub,
|
||||
request: SignTransactionsRequest,
|
||||
): Future[SignTransactionsResponse] = service.signTransactions(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: SignTransactionsResponse
|
||||
): Either[String, Seq[GenericSignedTopologyTransactionX]] =
|
||||
response.transactions.traverse(SignedTopologyTransactionX.fromProtoV2).leftMap(_.message)
|
||||
}
|
||||
|
||||
final case class Propose[M <: TopologyMappingX: ClassTag](
|
||||
mapping: Either[String, M],
|
||||
signedBy: Seq[Fingerprint],
|
||||
change: TopologyChangeOpX,
|
||||
serial: Option[PositiveInt],
|
||||
mustFullyAuthorize: Boolean,
|
||||
forceChange: Boolean,
|
||||
store: String,
|
||||
) extends BaseWriteCommand[
|
||||
AuthorizeRequest,
|
||||
AuthorizeResponse,
|
||||
SignedTopologyTransactionX[TopologyChangeOpX, M],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, AuthorizeRequest] = mapping.map(m =>
|
||||
AuthorizeRequest(
|
||||
Proposal(
|
||||
AuthorizeRequest.Proposal(
|
||||
change.toProto,
|
||||
serial.map(_.value).getOrElse(0),
|
||||
Some(m.toProtoV2),
|
||||
)
|
||||
),
|
||||
mustFullyAuthorize = mustFullyAuthorize,
|
||||
forceChange = false,
|
||||
signedBy = signedBy.map(_.toProtoPrimitive),
|
||||
store,
|
||||
)
|
||||
)
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceXStub,
|
||||
request: AuthorizeRequest,
|
||||
): Future[AuthorizeResponse] = service.authorize(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: AuthorizeResponse
|
||||
): Either[String, SignedTopologyTransactionX[TopologyChangeOpX, M]] = response.transaction
|
||||
.toRight("no transaction in response")
|
||||
.flatMap(
|
||||
SignedTopologyTransactionX
|
||||
.fromProtoV2(_)
|
||||
.leftMap(_.message)
|
||||
.flatMap(tx =>
|
||||
tx.selectMapping[M]
|
||||
.toRight(
|
||||
s"Expected mapping ${ClassTag[M].getClass.getSimpleName}, but received: ${tx.transaction.mapping.getClass.getSimpleName}"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
object Propose {
|
||||
def apply[M <: TopologyMappingX: ClassTag](
|
||||
mapping: M,
|
||||
signedBy: Seq[Fingerprint],
|
||||
store: String,
|
||||
serial: Option[PositiveInt] = None,
|
||||
change: TopologyChangeOpX = TopologyChangeOpX.Replace,
|
||||
mustFullyAuthorize: Boolean = false,
|
||||
forceChange: Boolean = false,
|
||||
): Propose[M] =
|
||||
Propose(Right(mapping), signedBy, change, serial, mustFullyAuthorize, forceChange, store)
|
||||
|
||||
}
|
||||
|
||||
final case class Authorize[M <: TopologyMappingX: ClassTag](
|
||||
transactionHash: String,
|
||||
mustFullyAuthorize: Boolean,
|
||||
signedBy: Seq[Fingerprint],
|
||||
store: String,
|
||||
) extends BaseWriteCommand[
|
||||
AuthorizeRequest,
|
||||
AuthorizeResponse,
|
||||
SignedTopologyTransactionX[TopologyChangeOpX, M],
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, AuthorizeRequest] = Right(
|
||||
AuthorizeRequest(
|
||||
TransactionHash(transactionHash),
|
||||
mustFullyAuthorize = mustFullyAuthorize,
|
||||
forceChange = false,
|
||||
signedBy = signedBy.map(_.toProtoPrimitive),
|
||||
store = store,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: TopologyManagerWriteServiceXStub,
|
||||
request: AuthorizeRequest,
|
||||
): Future[AuthorizeResponse] = service.authorize(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: AuthorizeResponse
|
||||
): Either[String, SignedTopologyTransactionX[TopologyChangeOpX, M]] = response.transaction
|
||||
.toRight("no transaction in response")
|
||||
.flatMap(
|
||||
SignedTopologyTransactionX
|
||||
.fromProtoV2(_)
|
||||
.leftMap(_.message)
|
||||
.flatMap(tx =>
|
||||
tx.selectMapping[M]
|
||||
.toRight(
|
||||
s"Expected mapping ${ClassTag[M].getClass.getSimpleName}, but received: ${tx.transaction.mapping.getClass.getSimpleName}"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object Init {
|
||||
|
||||
abstract class BaseInitializationService[Req, Resp, Res]
|
||||
extends GrpcAdminCommand[Req, Resp, Res] {
|
||||
override type Svc = IdentityInitializationServiceXStub
|
||||
override def createService(channel: ManagedChannel): IdentityInitializationServiceXStub =
|
||||
v1.IdentityInitializationServiceXGrpc.stub(channel)
|
||||
}
|
||||
|
||||
final case class InitId(identifier: String)
|
||||
extends BaseInitializationService[v1.InitIdRequest, v1.InitIdResponse, Unit] {
|
||||
|
||||
override def createRequest(): Either[String, v1.InitIdRequest] =
|
||||
Right(v1.InitIdRequest(identifier))
|
||||
|
||||
override def submitRequest(
|
||||
service: IdentityInitializationServiceXStub,
|
||||
request: v1.InitIdRequest,
|
||||
): Future[v1.InitIdResponse] =
|
||||
service.initId(request)
|
||||
|
||||
override def handleResponse(response: v1.InitIdResponse): Either[String, Unit] =
|
||||
Right(())
|
||||
}
|
||||
|
||||
final case class GetId()
|
||||
extends BaseInitializationService[Empty, v1.GetIdResponse, UniqueIdentifier] {
|
||||
override def createRequest(): Either[String, Empty] =
|
||||
Right(Empty())
|
||||
|
||||
override def submitRequest(
|
||||
service: IdentityInitializationServiceXStub,
|
||||
request: Empty,
|
||||
): Future[v1.GetIdResponse] =
|
||||
service.getId(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v1.GetIdResponse
|
||||
): Either[String, UniqueIdentifier] = {
|
||||
if (response.uniqueIdentifier.nonEmpty)
|
||||
UniqueIdentifier.fromProtoPrimitive_(response.uniqueIdentifier)
|
||||
else
|
||||
Left(
|
||||
s"Node is not initialized and therefore does not have an Id assigned yet."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,371 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
DefaultUnboundedTimeout,
|
||||
TimeoutType,
|
||||
}
|
||||
import com.digitalasset.canton.crypto.admin.grpc.PrivateKeyMetadata
|
||||
import com.digitalasset.canton.crypto.admin.v0
|
||||
import com.digitalasset.canton.crypto.admin.v0.VaultServiceGrpc.VaultServiceStub
|
||||
import com.digitalasset.canton.crypto.{PublicKeyWithName, v0 as cryptoproto, *}
|
||||
import com.digitalasset.canton.util.{EitherUtil, OptionUtil}
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.empty.Empty
|
||||
import io.grpc.ManagedChannel
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
object VaultAdminCommands {
|
||||
|
||||
abstract class BaseVaultAdminCommand[Req, Res, Result]
|
||||
extends GrpcAdminCommand[Req, Res, Result] {
|
||||
override type Svc = VaultServiceStub
|
||||
override def createService(channel: ManagedChannel): VaultServiceStub =
|
||||
v0.VaultServiceGrpc.stub(channel)
|
||||
}
|
||||
|
||||
abstract class ListKeys[R, T](
|
||||
filterFingerprint: String,
|
||||
filterName: String,
|
||||
filterPurpose: Set[KeyPurpose] = Set.empty,
|
||||
) extends BaseVaultAdminCommand[v0.ListKeysRequest, R, Seq[T]] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ListKeysRequest] =
|
||||
Right(
|
||||
v0.ListKeysRequest(
|
||||
filterFingerprint = filterFingerprint,
|
||||
filterName = filterName,
|
||||
filterPurpose = filterPurpose.map(_.toProtoEnum).toSeq,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// list keys in my key vault
|
||||
final case class ListMyKeys(
|
||||
filterFingerprint: String,
|
||||
filterName: String,
|
||||
filterPurpose: Set[KeyPurpose] = Set.empty,
|
||||
) extends ListKeys[v0.ListMyKeysResponse, PrivateKeyMetadata](
|
||||
filterFingerprint,
|
||||
filterName,
|
||||
filterPurpose,
|
||||
) {
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.ListKeysRequest,
|
||||
): Future[v0.ListMyKeysResponse] =
|
||||
service.listMyKeys(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListMyKeysResponse
|
||||
): Either[String, Seq[PrivateKeyMetadata]] =
|
||||
response.privateKeysMetadata.traverse(PrivateKeyMetadata.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
// list public keys in key registry
|
||||
final case class ListPublicKeys(
|
||||
filterFingerprint: String,
|
||||
filterName: String,
|
||||
filterPurpose: Set[KeyPurpose] = Set.empty,
|
||||
) extends ListKeys[v0.ListKeysResponse, PublicKeyWithName](
|
||||
filterFingerprint,
|
||||
filterName,
|
||||
filterPurpose,
|
||||
) {
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.ListKeysRequest,
|
||||
): Future[v0.ListKeysResponse] =
|
||||
service.listPublicKeys(request)
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.ListKeysResponse
|
||||
): Either[String, Seq[PublicKeyWithName]] =
|
||||
response.publicKeys.traverse(PublicKeyWithName.fromProtoV0).leftMap(_.toString)
|
||||
}
|
||||
|
||||
abstract class BaseImportPublicKey
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.ImportPublicKeyRequest,
|
||||
v0.ImportPublicKeyResponse,
|
||||
Fingerprint,
|
||||
] {
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.ImportPublicKeyRequest,
|
||||
): Future[v0.ImportPublicKeyResponse] =
|
||||
service.importPublicKey(request)
|
||||
|
||||
override def handleResponse(response: v0.ImportPublicKeyResponse): Either[String, Fingerprint] =
|
||||
Fingerprint.fromProtoPrimitive(response.fingerprint).leftMap(_.toString)
|
||||
}
|
||||
|
||||
// upload a public key into the key registry
|
||||
final case class ImportPublicKey(publicKey: ByteString, name: Option[String])
|
||||
extends BaseImportPublicKey {
|
||||
|
||||
override def createRequest(): Either[String, v0.ImportPublicKeyRequest] =
|
||||
Right(v0.ImportPublicKeyRequest(publicKey = publicKey, name = name.getOrElse("")))
|
||||
}
|
||||
|
||||
final case class GenerateSigningKey(name: String, scheme: Option[SigningKeyScheme])
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.GenerateSigningKeyRequest,
|
||||
v0.GenerateSigningKeyResponse,
|
||||
SigningPublicKey,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.GenerateSigningKeyRequest] =
|
||||
Right(
|
||||
v0.GenerateSigningKeyRequest(
|
||||
name = name,
|
||||
keyScheme = scheme.fold[cryptoproto.SigningKeyScheme](
|
||||
cryptoproto.SigningKeyScheme.MissingSigningKeyScheme
|
||||
)(_.toProtoEnum),
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.GenerateSigningKeyRequest,
|
||||
): Future[v0.GenerateSigningKeyResponse] = {
|
||||
service.generateSigningKey(request)
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.GenerateSigningKeyResponse
|
||||
): Either[String, SigningPublicKey] =
|
||||
response.publicKey
|
||||
.toRight("No public key returned")
|
||||
.flatMap(k => SigningPublicKey.fromProtoV0(k).leftMap(_.toString))
|
||||
|
||||
// may take some time if we need to wait for entropy
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
final case class GenerateEncryptionKey(name: String, scheme: Option[EncryptionKeyScheme])
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.GenerateEncryptionKeyRequest,
|
||||
v0.GenerateEncryptionKeyResponse,
|
||||
EncryptionPublicKey,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.GenerateEncryptionKeyRequest] =
|
||||
Right(
|
||||
v0.GenerateEncryptionKeyRequest(
|
||||
name = name,
|
||||
keyScheme = scheme.fold[cryptoproto.EncryptionKeyScheme](
|
||||
cryptoproto.EncryptionKeyScheme.MissingEncryptionKeyScheme
|
||||
)(_.toProtoEnum),
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.GenerateEncryptionKeyRequest,
|
||||
): Future[v0.GenerateEncryptionKeyResponse] = {
|
||||
service.generateEncryptionKey(request)
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.GenerateEncryptionKeyResponse
|
||||
): Either[String, EncryptionPublicKey] =
|
||||
response.publicKey
|
||||
.toRight("No public key returned")
|
||||
.flatMap(k => EncryptionPublicKey.fromProtoV0(k).leftMap(_.toString))
|
||||
|
||||
// may time some time if we need to wait for entropy
|
||||
override def timeoutType: TimeoutType = DefaultUnboundedTimeout
|
||||
|
||||
}
|
||||
|
||||
final case class RegisterKmsSigningKey(kmsKeyId: String, name: String)
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.RegisterKmsSigningKeyRequest,
|
||||
v0.RegisterKmsSigningKeyResponse,
|
||||
SigningPublicKey,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.RegisterKmsSigningKeyRequest] =
|
||||
Right(
|
||||
v0.RegisterKmsSigningKeyRequest(
|
||||
kmsKeyId = kmsKeyId,
|
||||
name = name,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.RegisterKmsSigningKeyRequest,
|
||||
): Future[v0.RegisterKmsSigningKeyResponse] = {
|
||||
service.registerKmsSigningKey(request)
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.RegisterKmsSigningKeyResponse
|
||||
): Either[String, SigningPublicKey] =
|
||||
response.publicKey
|
||||
.toRight("No public key returned")
|
||||
.flatMap(k => SigningPublicKey.fromProtoV0(k).leftMap(_.toString))
|
||||
|
||||
}
|
||||
|
||||
final case class RegisterKmsEncryptionKey(kmsKeyId: String, name: String)
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.RegisterKmsEncryptionKeyRequest,
|
||||
v0.RegisterKmsEncryptionKeyResponse,
|
||||
EncryptionPublicKey,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.RegisterKmsEncryptionKeyRequest] =
|
||||
Right(
|
||||
v0.RegisterKmsEncryptionKeyRequest(
|
||||
kmsKeyId = kmsKeyId,
|
||||
name = name,
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.RegisterKmsEncryptionKeyRequest,
|
||||
): Future[v0.RegisterKmsEncryptionKeyResponse] = {
|
||||
service.registerKmsEncryptionKey(request)
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.RegisterKmsEncryptionKeyResponse
|
||||
): Either[String, EncryptionPublicKey] =
|
||||
response.publicKey
|
||||
.toRight("No public key returned")
|
||||
.flatMap(k => EncryptionPublicKey.fromProtoV0(k).leftMap(_.toString))
|
||||
|
||||
}
|
||||
|
||||
final case class RotateWrapperKey(newWrapperKeyId: String)
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.RotateWrapperKeyRequest,
|
||||
Empty,
|
||||
Unit,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.RotateWrapperKeyRequest] =
|
||||
Right(
|
||||
v0.RotateWrapperKeyRequest(
|
||||
newWrapperKeyId = newWrapperKeyId
|
||||
)
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.RotateWrapperKeyRequest,
|
||||
): Future[Empty] = {
|
||||
service.rotateWrapperKey(request)
|
||||
}
|
||||
|
||||
override def handleResponse(response: Empty): Either[String, Unit] = Right(())
|
||||
|
||||
}
|
||||
|
||||
final case class GetWrapperKeyId()
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.GetWrapperKeyIdRequest,
|
||||
v0.GetWrapperKeyIdResponse,
|
||||
String,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.GetWrapperKeyIdRequest] =
|
||||
Right(
|
||||
v0.GetWrapperKeyIdRequest()
|
||||
)
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.GetWrapperKeyIdRequest,
|
||||
): Future[v0.GetWrapperKeyIdResponse] = {
|
||||
service.getWrapperKeyId(request)
|
||||
}
|
||||
|
||||
override def handleResponse(
|
||||
response: v0.GetWrapperKeyIdResponse
|
||||
): Either[String, String] =
|
||||
Right(response.wrapperKeyId)
|
||||
|
||||
}
|
||||
|
||||
final case class ImportKeyPair(keyPair: ByteString, name: Option[String])
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.ImportKeyPairRequest,
|
||||
v0.ImportKeyPairResponse,
|
||||
Unit,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ImportKeyPairRequest] =
|
||||
Right(v0.ImportKeyPairRequest(keyPair = keyPair, name = OptionUtil.noneAsEmptyString(name)))
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.ImportKeyPairRequest,
|
||||
): Future[v0.ImportKeyPairResponse] =
|
||||
service.importKeyPair(request)
|
||||
|
||||
override def handleResponse(response: v0.ImportKeyPairResponse): Either[String, Unit] =
|
||||
EitherUtil.unit
|
||||
}
|
||||
|
||||
final case class ExportKeyPair(fingerprint: Fingerprint, protocolVersion: ProtocolVersion)
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.ExportKeyPairRequest,
|
||||
v0.ExportKeyPairResponse,
|
||||
ByteString,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.ExportKeyPairRequest] = {
|
||||
Right(
|
||||
v0.ExportKeyPairRequest(
|
||||
fingerprint = fingerprint.toProtoPrimitive,
|
||||
protocolVersion = protocolVersion.toProtoPrimitive,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.ExportKeyPairRequest,
|
||||
): Future[v0.ExportKeyPairResponse] =
|
||||
service.exportKeyPair(request)
|
||||
|
||||
override def handleResponse(response: v0.ExportKeyPairResponse): Either[String, ByteString] =
|
||||
Right(response.keyPair)
|
||||
}
|
||||
|
||||
final case class DeleteKeyPair(fingerprint: Fingerprint)
|
||||
extends BaseVaultAdminCommand[
|
||||
v0.DeleteKeyPairRequest,
|
||||
v0.DeleteKeyPairResponse,
|
||||
Unit,
|
||||
] {
|
||||
|
||||
override def createRequest(): Either[String, v0.DeleteKeyPairRequest] = {
|
||||
Right(v0.DeleteKeyPairRequest(fingerprint = fingerprint.toProtoPrimitive))
|
||||
}
|
||||
|
||||
override def submitRequest(
|
||||
service: VaultServiceStub,
|
||||
request: v0.DeleteKeyPairRequest,
|
||||
): Future[v0.DeleteKeyPairResponse] =
|
||||
service.deleteKeyPair(request)
|
||||
|
||||
override def handleResponse(response: v0.DeleteKeyPairResponse): Either[String, Unit] =
|
||||
EitherUtil.unit
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import cats.Show
|
||||
import com.digitalasset.canton.admin.api.client.data.CantonStatus.splitSuccessfulAndFailedStatus
|
||||
import com.digitalasset.canton.console.{DomainReference, ParticipantReference}
|
||||
import com.digitalasset.canton.health.admin.data.{DomainStatus, NodeStatus, ParticipantStatus}
|
||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.util.ShowUtil.*
|
||||
|
||||
trait CantonStatus extends PrettyPrinting {
|
||||
protected def descriptions[Status <: NodeStatus.Status](
|
||||
statusMap: Map[String, Status],
|
||||
failureMap: Map[String, NodeStatus.Failure],
|
||||
instanceType: String,
|
||||
): Seq[String] = {
|
||||
|
||||
val success = sort(statusMap)
|
||||
.map { case (d, status) =>
|
||||
show"Status for ${instanceType.unquoted} ${d.singleQuoted}:\n$status"
|
||||
}
|
||||
|
||||
val failure = sort(failureMap)
|
||||
.map { case (d, status) =>
|
||||
show"${instanceType.unquoted} ${d.singleQuoted} cannot be reached: ${status.msg}"
|
||||
}
|
||||
|
||||
success ++ failure
|
||||
}
|
||||
|
||||
private def sort[K: Ordering, V](status: Map[K, V]): Seq[(K, V)] =
|
||||
status.toSeq.sortBy(_._1)
|
||||
}
|
||||
|
||||
object CantonStatus {
|
||||
def splitSuccessfulAndFailedStatus[K: Show, S <: NodeStatus.Status](
|
||||
nodes: Map[K, () => NodeStatus[S]],
|
||||
instanceType: String,
|
||||
): (Map[K, S], Map[K, NodeStatus.Failure]) = {
|
||||
val map: Map[K, NodeStatus[S]] =
|
||||
nodes.map { case (node, getStatus) =>
|
||||
node -> getStatus()
|
||||
}
|
||||
val status: Map[K, S] =
|
||||
map.collect { case (n, NodeStatus.Success(status)) =>
|
||||
n -> status
|
||||
}
|
||||
val unreachable: Map[K, NodeStatus.Failure] =
|
||||
map.collect {
|
||||
case (s, entry: NodeStatus.Failure) => s -> entry
|
||||
case (s, _: NodeStatus.NotInitialized) =>
|
||||
s -> NodeStatus.Failure(
|
||||
s"${instanceType.unquoted} ${s.show.singleQuoted} has not been initialized"
|
||||
)
|
||||
}
|
||||
(status, unreachable)
|
||||
}
|
||||
}
|
||||
|
||||
object CommunityCantonStatus {
|
||||
def getStatus(
|
||||
domains: Map[String, () => NodeStatus[DomainStatus]],
|
||||
participants: Map[String, () => NodeStatus[ParticipantStatus]],
|
||||
): CommunityCantonStatus = {
|
||||
val (domainStatus, unreachableDomains) =
|
||||
splitSuccessfulAndFailedStatus(domains, DomainReference.InstanceType)
|
||||
val (participantStatus, unreachableParticipants) =
|
||||
splitSuccessfulAndFailedStatus(participants, ParticipantReference.InstanceType)
|
||||
|
||||
CommunityCantonStatus(
|
||||
domainStatus,
|
||||
unreachableDomains,
|
||||
participantStatus,
|
||||
unreachableParticipants,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final case class CommunityCantonStatus(
|
||||
domainStatus: Map[String, DomainStatus],
|
||||
unreachableDomains: Map[String, NodeStatus.Failure],
|
||||
participantStatus: Map[String, ParticipantStatus],
|
||||
unreachableParticipants: Map[String, NodeStatus.Failure],
|
||||
) extends CantonStatus {
|
||||
def tupled: (Map[String, DomainStatus], Map[String, ParticipantStatus]) =
|
||||
(domainStatus, participantStatus)
|
||||
|
||||
override def pretty: Pretty[CommunityCantonStatus] = prettyOfString { _ =>
|
||||
val domains = descriptions(
|
||||
domainStatus,
|
||||
unreachableDomains,
|
||||
DomainReference.InstanceType,
|
||||
)
|
||||
val participants =
|
||||
descriptions(
|
||||
participantStatus,
|
||||
unreachableParticipants,
|
||||
ParticipantReference.InstanceType,
|
||||
)
|
||||
(domains ++ participants).mkString(System.lineSeparator() * 2)
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import com.digitalasset.canton.DomainAlias
|
||||
import com.digitalasset.canton.participant.admin.{v0 as participantAdminV0}
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.topology.*
|
||||
|
||||
final case class ListConnectedDomainsResult(
|
||||
domainAlias: DomainAlias,
|
||||
domainId: DomainId,
|
||||
healthy: Boolean,
|
||||
)
|
||||
|
||||
object ListConnectedDomainsResult {
|
||||
def fromProtoV0(
|
||||
value: participantAdminV0.ListConnectedDomainsResponse.Result
|
||||
): ParsingResult[ListConnectedDomainsResult] = {
|
||||
val participantAdminV0.ListConnectedDomainsResponse.Result(domainAlias, domainId, healthy) =
|
||||
value
|
||||
for {
|
||||
domainId <- DomainId.fromProtoPrimitive(domainId, "domainId")
|
||||
domainAlias <- DomainAlias.fromProtoPrimitive(domainAlias)
|
||||
|
||||
} yield ListConnectedDomainsResult(
|
||||
domainAlias = domainAlias,
|
||||
domainId = domainId,
|
||||
healthy = healthy,
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
final case class DarMetadata(
|
||||
name: String,
|
||||
main: String,
|
||||
packages: Seq[String],
|
||||
dependencies: Seq[String],
|
||||
)
|
||||
|
||||
object DarMetadata {
|
||||
def fromProtoV0(
|
||||
value: participantAdminV0.ListDarContentsResponse
|
||||
): ParsingResult[DarMetadata] = {
|
||||
val participantAdminV0.ListDarContentsResponse(description, main, packages, dependencies) =
|
||||
value
|
||||
Right(DarMetadata(description, main, packages, dependencies))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import cats.syntax.either.*
|
||||
import com.daml.nonempty.NonEmptyUtil
|
||||
import com.digitalasset.canton.admin.api.client.data.crypto.*
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
|
||||
import com.digitalasset.canton.config.{NonNegativeFiniteDuration, PositiveDurationSeconds}
|
||||
import com.digitalasset.canton.protocol.DynamicDomainParameters.InvalidDynamicDomainParameters
|
||||
import com.digitalasset.canton.protocol.{
|
||||
DynamicDomainParameters as DynamicDomainParametersInternal,
|
||||
StaticDomainParameters as StaticDomainParametersInternal,
|
||||
v2 as protocolV2,
|
||||
}
|
||||
import com.digitalasset.canton.topology.admin.v0.DomainParametersChangeAuthorization
|
||||
import com.digitalasset.canton.util.BinaryFileUtil
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
import com.digitalasset.canton.{crypto as DomainCrypto}
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import io.scalaland.chimney.dsl.*
|
||||
|
||||
import scala.Ordering.Implicits.*
|
||||
|
||||
final case class StaticDomainParameters(
|
||||
uniqueContractKeys: Boolean,
|
||||
requiredSigningKeySchemes: Set[SigningKeyScheme],
|
||||
requiredEncryptionKeySchemes: Set[EncryptionKeyScheme],
|
||||
requiredSymmetricKeySchemes: Set[SymmetricKeyScheme],
|
||||
requiredHashAlgorithms: Set[HashAlgorithm],
|
||||
requiredCryptoKeyFormats: Set[CryptoKeyFormat],
|
||||
protocolVersion: ProtocolVersion,
|
||||
) {
|
||||
def writeToFile(outputFile: String): Unit =
|
||||
BinaryFileUtil.writeByteStringToFile(outputFile, toInternal.toByteString)
|
||||
|
||||
private[canton] def toInternal: StaticDomainParametersInternal =
|
||||
StaticDomainParametersInternal.create(
|
||||
uniqueContractKeys = uniqueContractKeys,
|
||||
requiredSigningKeySchemes = NonEmptyUtil.fromUnsafe(
|
||||
requiredSigningKeySchemes.map(_.transformInto[DomainCrypto.SigningKeyScheme])
|
||||
),
|
||||
requiredEncryptionKeySchemes = NonEmptyUtil.fromUnsafe(
|
||||
requiredEncryptionKeySchemes.map(_.transformInto[DomainCrypto.EncryptionKeyScheme])
|
||||
),
|
||||
requiredSymmetricKeySchemes = NonEmptyUtil.fromUnsafe(
|
||||
requiredSymmetricKeySchemes.map(_.transformInto[DomainCrypto.SymmetricKeyScheme])
|
||||
),
|
||||
requiredHashAlgorithms = NonEmptyUtil.fromUnsafe(
|
||||
requiredHashAlgorithms.map(_.transformInto[DomainCrypto.HashAlgorithm])
|
||||
),
|
||||
requiredCryptoKeyFormats = NonEmptyUtil.fromUnsafe(
|
||||
requiredCryptoKeyFormats.map(_.transformInto[DomainCrypto.CryptoKeyFormat])
|
||||
),
|
||||
protocolVersion = protocolVersion,
|
||||
)
|
||||
}
|
||||
|
||||
object StaticDomainParameters {
|
||||
|
||||
def apply(
|
||||
domain: StaticDomainParametersInternal
|
||||
): StaticDomainParameters =
|
||||
StaticDomainParameters(
|
||||
uniqueContractKeys = domain.uniqueContractKeys,
|
||||
requiredSigningKeySchemes =
|
||||
domain.requiredSigningKeySchemes.forgetNE.map(_.transformInto[SigningKeyScheme]),
|
||||
requiredEncryptionKeySchemes =
|
||||
domain.requiredEncryptionKeySchemes.forgetNE.map(_.transformInto[EncryptionKeyScheme]),
|
||||
requiredSymmetricKeySchemes =
|
||||
domain.requiredSymmetricKeySchemes.forgetNE.map(_.transformInto[SymmetricKeyScheme]),
|
||||
requiredHashAlgorithms =
|
||||
domain.requiredHashAlgorithms.forgetNE.map(_.transformInto[HashAlgorithm]),
|
||||
requiredCryptoKeyFormats =
|
||||
domain.requiredCryptoKeyFormats.forgetNE.map(_.transformInto[CryptoKeyFormat]),
|
||||
protocolVersion = domain.protocolVersion,
|
||||
)
|
||||
|
||||
def tryReadFromFile(inputFile: String): StaticDomainParameters = {
|
||||
val staticDomainParametersInternal = StaticDomainParametersInternal
|
||||
.readFromFile(inputFile)
|
||||
.valueOr(err =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Reading static domain parameters from file $inputFile failed: $err"
|
||||
)
|
||||
)
|
||||
|
||||
StaticDomainParameters(staticDomainParametersInternal)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(#15650) Properly expose new BFT parameters and domain limits
|
||||
final case class DynamicDomainParameters(
|
||||
participantResponseTimeout: NonNegativeFiniteDuration,
|
||||
mediatorReactionTimeout: NonNegativeFiniteDuration,
|
||||
transferExclusivityTimeout: NonNegativeFiniteDuration,
|
||||
topologyChangeDelay: NonNegativeFiniteDuration,
|
||||
ledgerTimeRecordTimeTolerance: NonNegativeFiniteDuration,
|
||||
mediatorDeduplicationTimeout: NonNegativeFiniteDuration,
|
||||
reconciliationInterval: PositiveDurationSeconds,
|
||||
maxRatePerParticipant: NonNegativeInt,
|
||||
maxRequestSize: NonNegativeInt,
|
||||
sequencerAggregateSubmissionTimeout: NonNegativeFiniteDuration,
|
||||
) {
|
||||
|
||||
if (ledgerTimeRecordTimeTolerance * 2 > mediatorDeduplicationTimeout)
|
||||
throw new InvalidDynamicDomainParameters(
|
||||
s"The ledgerTimeRecordTimeTolerance ($ledgerTimeRecordTimeTolerance) must be at most half of the " +
|
||||
s"mediatorDeduplicationTimeout ($mediatorDeduplicationTimeout)."
|
||||
)
|
||||
|
||||
// https://docs.google.com/document/d/1tpPbzv2s6bjbekVGBn6X5VZuw0oOTHek5c30CBo4UkI/edit#bookmark=id.1dzc6dxxlpca
|
||||
private[canton] def compatibleWithNewLedgerTimeRecordTimeTolerance(
|
||||
newLedgerTimeRecordTimeTolerance: NonNegativeFiniteDuration
|
||||
): Boolean = {
|
||||
// If false, a new request may receive the same ledger time as a previous request and the previous
|
||||
// request may be evicted too early from the mediator's deduplication store.
|
||||
// Thus, an attacker may assign the same UUID to both requests.
|
||||
// See i9028 for a detailed design. (This is the second clause of item 2 of Lemma 2).
|
||||
ledgerTimeRecordTimeTolerance + newLedgerTimeRecordTimeTolerance <= mediatorDeduplicationTimeout
|
||||
}
|
||||
|
||||
def update(
|
||||
participantResponseTimeout: NonNegativeFiniteDuration = participantResponseTimeout,
|
||||
mediatorReactionTimeout: NonNegativeFiniteDuration = mediatorReactionTimeout,
|
||||
transferExclusivityTimeout: NonNegativeFiniteDuration = transferExclusivityTimeout,
|
||||
topologyChangeDelay: NonNegativeFiniteDuration = topologyChangeDelay,
|
||||
ledgerTimeRecordTimeTolerance: NonNegativeFiniteDuration = ledgerTimeRecordTimeTolerance,
|
||||
): DynamicDomainParameters = this.copy(
|
||||
participantResponseTimeout = participantResponseTimeout,
|
||||
mediatorReactionTimeout = mediatorReactionTimeout,
|
||||
transferExclusivityTimeout = transferExclusivityTimeout,
|
||||
topologyChangeDelay = topologyChangeDelay,
|
||||
ledgerTimeRecordTimeTolerance = ledgerTimeRecordTimeTolerance,
|
||||
)
|
||||
|
||||
def toProto: DomainParametersChangeAuthorization.Parameters =
|
||||
DomainParametersChangeAuthorization.Parameters.ParametersV1(
|
||||
protocolV2.DynamicDomainParameters(
|
||||
participantResponseTimeout = Some(participantResponseTimeout.toProtoPrimitive),
|
||||
mediatorReactionTimeout = Some(mediatorReactionTimeout.toProtoPrimitive),
|
||||
transferExclusivityTimeout = Some(transferExclusivityTimeout.toProtoPrimitive),
|
||||
topologyChangeDelay = Some(topologyChangeDelay.toProtoPrimitive),
|
||||
ledgerTimeRecordTimeTolerance = Some(ledgerTimeRecordTimeTolerance.toProtoPrimitive),
|
||||
mediatorDeduplicationTimeout = Some(mediatorDeduplicationTimeout.toProtoPrimitive),
|
||||
reconciliationInterval = Some(reconciliationInterval.toProtoPrimitive),
|
||||
defaultParticipantLimits = Some(
|
||||
protocolV2.ParticipantDomainLimits(
|
||||
maxRate = maxRatePerParticipant.unwrap,
|
||||
maxNumParties = 0,
|
||||
maxNumPackages = 0,
|
||||
)
|
||||
),
|
||||
maxRequestSize = maxRequestSize.unwrap,
|
||||
permissionedDomain = false,
|
||||
requiredPackages = Nil,
|
||||
onlyRequiredPackagesPermitted = false,
|
||||
defaultMaxHostingParticipantsPerParty = 0,
|
||||
sequencerAggregateSubmissionTimeout =
|
||||
Some(sequencerAggregateSubmissionTimeout.toProtoPrimitive),
|
||||
trafficControlParameters = None,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
object DynamicDomainParameters {
|
||||
|
||||
/** Default dynamic domain parameters for non-static clocks */
|
||||
@VisibleForTesting
|
||||
def defaultValues(protocolVersion: ProtocolVersion): DynamicDomainParameters =
|
||||
DynamicDomainParameters(
|
||||
DynamicDomainParametersInternal.defaultValues(protocolVersion)
|
||||
)
|
||||
|
||||
def apply(
|
||||
domain: DynamicDomainParametersInternal
|
||||
): DynamicDomainParameters =
|
||||
domain.transformInto[DynamicDomainParameters]
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
final case class LedgerApiObjectMeta(
|
||||
resourceVersion: String,
|
||||
annotations: Map[String, String],
|
||||
)
|
||||
|
||||
object LedgerApiObjectMeta {
|
||||
def empty: LedgerApiObjectMeta =
|
||||
LedgerApiObjectMeta(resourceVersion = "", annotations = Map.empty)
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import com.daml.ledger.api.v1.admin.metering_report_service.GetMeteringReportResponse
|
||||
import com.digitalasset.canton.serialization.ProtoConverter
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.google.protobuf.struct
|
||||
import com.google.protobuf.struct.Value.Kind
|
||||
import com.google.protobuf.struct.{ListValue, Struct}
|
||||
import io.circe.Decoder.Result
|
||||
import io.circe.Json.*
|
||||
import io.circe.*
|
||||
|
||||
object LedgerMeteringReport {
|
||||
|
||||
def fromProtoV0(
|
||||
value: GetMeteringReportResponse
|
||||
): ParsingResult[String] = {
|
||||
|
||||
for {
|
||||
s <- ProtoConverter.required("meteringReportJson", value.meteringReportJson)
|
||||
} yield {
|
||||
StructEncoderDecoder(s).spaces2
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
object StructEncoderDecoder extends Encoder[struct.Struct] with Decoder[struct.Struct] {
|
||||
|
||||
override def apply(s: struct.Struct): Json = {
|
||||
write(struct.Value.of(Kind.StructValue(s)))
|
||||
}
|
||||
|
||||
override def apply(c: HCursor): Result[struct.Struct] = {
|
||||
val value = read(c.value)
|
||||
if (value.kind.isStructValue) Right(value.getStructValue)
|
||||
else Left(DecodingFailure(s"Expected struct, not $value", Nil))
|
||||
}
|
||||
|
||||
private def write(value: struct.Value): Json = {
|
||||
value.kind match {
|
||||
case Kind.BoolValue(v) => Json.fromBoolean(v)
|
||||
case Kind.ListValue(v) => Json.fromValues(v.values.map(write))
|
||||
case Kind.NumberValue(v) => Json.fromDoubleOrNull(v)
|
||||
case Kind.StringValue(v) => Json.fromString(v)
|
||||
case Kind.StructValue(v) => Json.fromFields(v.fields.view.mapValues(write))
|
||||
case Kind.Empty | Kind.NullValue(_) => Json.Null
|
||||
}
|
||||
}
|
||||
|
||||
object StructFolder extends Folder[Kind] {
|
||||
def onNull = Kind.NullValue(struct.NullValue.NULL_VALUE)
|
||||
def onBoolean(value: Boolean) = Kind.BoolValue(value)
|
||||
def onNumber(value: JsonNumber) = Kind.NumberValue(value.toDouble)
|
||||
def onString(value: String) = Kind.StringValue(value)
|
||||
def onArray(value: Vector[Json]) = Kind.ListValue(ListValue(value.map(read)))
|
||||
def onObject(value: JsonObject) =
|
||||
Kind.StructValue(Struct.of(value.toMap.view.mapValues(read).toMap))
|
||||
}
|
||||
|
||||
private def read(c: Json): struct.Value = struct.Value.of(c.foldWith(StructFolder))
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import com.daml.ledger.api.v1.admin.object_meta.ObjectMeta as ProtoObjectMeta
|
||||
import com.daml.ledger.api.v1.admin.party_management_service.PartyDetails as ProtoPartyDetails
|
||||
import com.digitalasset.canton.topology.PartyId
|
||||
|
||||
import scala.util.control.NoStackTrace
|
||||
|
||||
/** Represents a party details value exposed in the Canton console
|
||||
*/
|
||||
final case class PartyDetails(
|
||||
party: PartyId,
|
||||
displayName: String,
|
||||
isLocal: Boolean,
|
||||
annotations: Map[String, String],
|
||||
identityProviderId: String,
|
||||
)
|
||||
|
||||
object PartyDetails {
|
||||
def fromProtoPartyDetails(details: ProtoPartyDetails): PartyDetails = PartyDetails(
|
||||
party = PartyId.tryFromProtoPrimitive(details.party),
|
||||
displayName = details.displayName,
|
||||
isLocal = details.isLocal,
|
||||
annotations = details.localMetadata.fold(Map.empty[String, String])(_.annotations),
|
||||
identityProviderId = details.identityProviderId,
|
||||
)
|
||||
def toProtoPartyDetails(
|
||||
details: PartyDetails,
|
||||
resourceVersionO: Option[String],
|
||||
): ProtoPartyDetails = ProtoPartyDetails(
|
||||
party = details.party.toString,
|
||||
displayName = details.displayName,
|
||||
isLocal = details.isLocal,
|
||||
localMetadata = Some(
|
||||
ProtoObjectMeta(
|
||||
resourceVersion = resourceVersionO.getOrElse(""),
|
||||
annotations = details.annotations,
|
||||
)
|
||||
),
|
||||
identityProviderId = details.identityProviderId,
|
||||
)
|
||||
}
|
||||
|
||||
final case class ModifyingNonModifiablePartyDetailsPropertiesError()
|
||||
extends RuntimeException("MODIFYING_AN_UNMODIFIABLE_PARTY_DETAILS_PROPERTY_ERROR")
|
||||
with NoStackTrace
|
@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import com.digitalasset.canton.pruning.admin.v0
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.{config, participant, scheduler}
|
||||
|
||||
final case class PruningSchedule(
|
||||
cron: String,
|
||||
maxDuration: config.PositiveDurationSeconds,
|
||||
retention: config.PositiveDurationSeconds,
|
||||
)
|
||||
|
||||
object PruningSchedule {
|
||||
private[admin] def fromProtoV0(scheduleP: v0.PruningSchedule): ParsingResult[PruningSchedule] =
|
||||
for {
|
||||
maxDuration <- config.PositiveDurationSeconds.fromProtoPrimitiveO("max_duration")(
|
||||
scheduleP.maxDuration
|
||||
)
|
||||
retention <- config.PositiveDurationSeconds.fromProtoPrimitiveO("retention")(
|
||||
scheduleP.retention
|
||||
)
|
||||
} yield PruningSchedule(scheduleP.cron, maxDuration, retention)
|
||||
|
||||
private[data] def fromInternal(
|
||||
internalSchedule: scheduler.PruningSchedule
|
||||
): PruningSchedule =
|
||||
PruningSchedule(
|
||||
internalSchedule.cron.toProtoPrimitive,
|
||||
config.PositiveDurationSeconds(internalSchedule.maxDuration.toScala),
|
||||
config.PositiveDurationSeconds(internalSchedule.retention.toScala),
|
||||
)
|
||||
}
|
||||
|
||||
final case class ParticipantPruningSchedule(
|
||||
schedule: PruningSchedule,
|
||||
pruneInternallyOnly: Boolean,
|
||||
)
|
||||
|
||||
object ParticipantPruningSchedule {
|
||||
private[admin] def fromProtoV0(
|
||||
participantSchedule: v0.ParticipantPruningSchedule
|
||||
): ParsingResult[ParticipantPruningSchedule] =
|
||||
for {
|
||||
internalSchedule <- participant.scheduler.ParticipantPruningSchedule.fromProtoV0(
|
||||
participantSchedule
|
||||
)
|
||||
} yield ParticipantPruningSchedule(
|
||||
PruningSchedule.fromInternal(internalSchedule.schedule),
|
||||
participantSchedule.pruneInternallyOnly,
|
||||
)
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import com.daml.ledger.api.v1.ValueOuterClass
|
||||
import com.daml.ledger.api.v1.value.Identifier
|
||||
import com.daml.ledger.javaapi
|
||||
|
||||
final case class TemplateId(
|
||||
packageId: String,
|
||||
moduleName: String,
|
||||
entityName: String,
|
||||
) {
|
||||
def toIdentifier: Identifier = Identifier(
|
||||
packageId = packageId,
|
||||
moduleName = moduleName,
|
||||
entityName = entityName,
|
||||
)
|
||||
|
||||
def toJavaIdentifier: javaapi.data.Identifier = new javaapi.data.Identifier(
|
||||
packageId,
|
||||
moduleName,
|
||||
entityName,
|
||||
)
|
||||
|
||||
def isModuleEntity(moduleName: String, entityName: String) =
|
||||
this.moduleName == moduleName && this.entityName == entityName
|
||||
}
|
||||
|
||||
object TemplateId {
|
||||
|
||||
def fromIdentifier(identifier: Identifier): TemplateId = {
|
||||
TemplateId(
|
||||
packageId = identifier.packageId,
|
||||
moduleName = identifier.moduleName,
|
||||
entityName = identifier.entityName,
|
||||
)
|
||||
}
|
||||
|
||||
def templateIdsFromJava(identifiers: javaapi.data.Identifier*): Seq[TemplateId] = {
|
||||
identifiers.map(fromJavaIdentifier)
|
||||
}
|
||||
|
||||
def fromJavaProtoIdentifier(templateId: ValueOuterClass.Identifier): TemplateId = {
|
||||
fromIdentifier(Identifier.fromJavaProto(templateId))
|
||||
}
|
||||
|
||||
def fromJavaIdentifier(templateId: javaapi.data.Identifier): TemplateId = {
|
||||
fromJavaProtoIdentifier(templateId.toProto)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import cats.syntax.traverse.*
|
||||
import com.digitalasset.canton.ProtoDeserializationError
|
||||
import com.digitalasset.canton.admin.api.client.data.ListPartiesResult.ParticipantDomains
|
||||
import com.digitalasset.canton.crypto.*
|
||||
import com.digitalasset.canton.protocol.{DynamicDomainParameters as DynamicDomainParametersInternal}
|
||||
import com.digitalasset.canton.serialization.ProtoConverter
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.topology.*
|
||||
import com.digitalasset.canton.topology.admin.v0
|
||||
import com.digitalasset.canton.topology.admin.v0.ListDomainParametersChangesResult.Result.Parameters
|
||||
import com.digitalasset.canton.topology.transaction.*
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
final case class ListPartiesResult(party: PartyId, participants: Seq[ParticipantDomains])
|
||||
|
||||
object ListPartiesResult {
|
||||
final case class DomainPermission(domain: DomainId, permission: ParticipantPermission)
|
||||
final case class ParticipantDomains(participant: ParticipantId, domains: Seq[DomainPermission])
|
||||
|
||||
private def fromProtoV0(
|
||||
value: v0.ListPartiesResponse.Result.ParticipantDomains.DomainPermissions
|
||||
): ParsingResult[DomainPermission] =
|
||||
for {
|
||||
domainId <- DomainId.fromProtoPrimitive(value.domain, "domain")
|
||||
permission <- ParticipantPermission.fromProtoEnum(value.permission)
|
||||
} yield DomainPermission(domainId, permission)
|
||||
|
||||
private def fromProtoV0(
|
||||
value: v0.ListPartiesResponse.Result.ParticipantDomains
|
||||
): ParsingResult[ParticipantDomains] =
|
||||
for {
|
||||
participantId <- ParticipantId.fromProtoPrimitive(value.participant, "participant")
|
||||
domains <- value.domains.traverse(fromProtoV0)
|
||||
} yield ParticipantDomains(participantId, domains)
|
||||
|
||||
def fromProtoV0(
|
||||
value: v0.ListPartiesResponse.Result
|
||||
): ParsingResult[ListPartiesResult] =
|
||||
for {
|
||||
partyUid <- UniqueIdentifier.fromProtoPrimitive(value.party, "party")
|
||||
participants <- value.participants.traverse(fromProtoV0)
|
||||
} yield ListPartiesResult(PartyId(partyUid), participants)
|
||||
}
|
||||
|
||||
final case class ListKeyOwnersResult(
|
||||
store: DomainId,
|
||||
owner: Member,
|
||||
signingKeys: Seq[SigningPublicKey],
|
||||
encryptionKeys: Seq[EncryptionPublicKey],
|
||||
) {
|
||||
def keys(purpose: KeyPurpose): Seq[PublicKey] = purpose match {
|
||||
case KeyPurpose.Signing => signingKeys
|
||||
case KeyPurpose.Encryption => encryptionKeys
|
||||
}
|
||||
}
|
||||
|
||||
object ListKeyOwnersResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListKeyOwnersResponse.Result
|
||||
): ParsingResult[ListKeyOwnersResult] =
|
||||
for {
|
||||
domain <- DomainId.fromProtoPrimitive(value.domain, "domain")
|
||||
owner <- Member.fromProtoPrimitive(value.keyOwner, "keyOwner")
|
||||
signingKeys <- value.signingKeys.traverse(SigningPublicKey.fromProtoV0)
|
||||
encryptionKeys <- value.encryptionKeys.traverse(EncryptionPublicKey.fromProtoV0)
|
||||
} yield ListKeyOwnersResult(domain, owner, signingKeys, encryptionKeys)
|
||||
}
|
||||
|
||||
final case class BaseResult(
|
||||
domain: String,
|
||||
validFrom: Instant,
|
||||
validUntil: Option[Instant],
|
||||
operation: TopologyChangeOp,
|
||||
serialized: ByteString,
|
||||
signedBy: Fingerprint,
|
||||
)
|
||||
|
||||
object BaseResult {
|
||||
def fromProtoV0(value: v0.BaseResult): ParsingResult[BaseResult] =
|
||||
for {
|
||||
protoValidFrom <- ProtoConverter.required("valid_from", value.validFrom)
|
||||
validFrom <- ProtoConverter.InstantConverter.fromProtoPrimitive(protoValidFrom)
|
||||
validUntil <- value.validUntil.traverse(ProtoConverter.InstantConverter.fromProtoPrimitive)
|
||||
operation <- TopologyChangeOp.fromProtoV0(value.operation)
|
||||
signedBy <- Fingerprint.fromProtoPrimitive(value.signedByFingerprint)
|
||||
|
||||
} yield BaseResult(value.store, validFrom, validUntil, operation, value.serialized, signedBy)
|
||||
}
|
||||
|
||||
final case class ListPartyToParticipantResult(context: BaseResult, item: PartyToParticipant)
|
||||
|
||||
object ListPartyToParticipantResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListPartyToParticipantResult.Result
|
||||
): ParsingResult[ListPartyToParticipantResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV0(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- PartyToParticipant.fromProtoV0(itemProto)
|
||||
} yield ListPartyToParticipantResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListOwnerToKeyMappingResult(
|
||||
context: BaseResult,
|
||||
item: OwnerToKeyMapping,
|
||||
key: Fingerprint,
|
||||
)
|
||||
|
||||
object ListOwnerToKeyMappingResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListOwnerToKeyMappingResult.Result
|
||||
): ParsingResult[ListOwnerToKeyMappingResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV0(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- OwnerToKeyMapping.fromProtoV0(itemProto)
|
||||
key <- Fingerprint.fromProtoPrimitive(value.keyFingerprint)
|
||||
} yield ListOwnerToKeyMappingResult(context, item, key)
|
||||
}
|
||||
|
||||
final case class ListNamespaceDelegationResult(
|
||||
context: BaseResult,
|
||||
item: NamespaceDelegation,
|
||||
targetKey: Fingerprint,
|
||||
)
|
||||
|
||||
object ListNamespaceDelegationResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListNamespaceDelegationResult.Result
|
||||
): ParsingResult[ListNamespaceDelegationResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV0(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- NamespaceDelegation.fromProtoV0(itemProto)
|
||||
targetKey <- Fingerprint.fromProtoPrimitive(value.targetKeyFingerprint)
|
||||
} yield ListNamespaceDelegationResult(context, item, targetKey)
|
||||
}
|
||||
|
||||
final case class ListIdentifierDelegationResult(
|
||||
context: BaseResult,
|
||||
item: IdentifierDelegation,
|
||||
targetKey: Fingerprint,
|
||||
)
|
||||
|
||||
object ListIdentifierDelegationResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListIdentifierDelegationResult.Result
|
||||
): ParsingResult[ListIdentifierDelegationResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV0(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- IdentifierDelegation.fromProtoV0(itemProto)
|
||||
targetKey <- Fingerprint.fromProtoPrimitive(value.targetKeyFingerprint)
|
||||
} yield ListIdentifierDelegationResult(context, item, targetKey)
|
||||
}
|
||||
|
||||
final case class ListSignedLegalIdentityClaimResult(context: BaseResult, item: LegalIdentityClaim)
|
||||
|
||||
object ListSignedLegalIdentityClaimResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListSignedLegalIdentityClaimResult.Result
|
||||
): ParsingResult[ListSignedLegalIdentityClaimResult] = {
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV0(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- SignedLegalIdentityClaim.fromProtoV0(itemProto)
|
||||
claim <- LegalIdentityClaim.fromByteString(item.claim)
|
||||
} yield ListSignedLegalIdentityClaimResult(context, claim)
|
||||
}
|
||||
}
|
||||
|
||||
final case class ListParticipantDomainStateResult(context: BaseResult, item: ParticipantState)
|
||||
|
||||
object ListParticipantDomainStateResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListParticipantDomainStateResult.Result
|
||||
): ParsingResult[ListParticipantDomainStateResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV0(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- ParticipantState.fromProtoV0(itemProto)
|
||||
} yield ListParticipantDomainStateResult(context, item)
|
||||
|
||||
}
|
||||
|
||||
final case class ListMediatorDomainStateResult(context: BaseResult, item: MediatorDomainState)
|
||||
|
||||
object ListMediatorDomainStateResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListMediatorDomainStateResult.Result
|
||||
): ParsingResult[ListMediatorDomainStateResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV0(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- MediatorDomainState.fromProtoV0(itemProto)
|
||||
} yield ListMediatorDomainStateResult(context, item)
|
||||
|
||||
}
|
||||
|
||||
final case class ListVettedPackagesResult(context: BaseResult, item: VettedPackages)
|
||||
|
||||
object ListVettedPackagesResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListVettedPackagesResult.Result
|
||||
): ParsingResult[ListVettedPackagesResult] = {
|
||||
val v0.ListVettedPackagesResult.Result(contextPO, itemPO) = value
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", contextPO)
|
||||
context <- BaseResult.fromProtoV0(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", itemPO)
|
||||
item <- VettedPackages.fromProtoV0(itemProto)
|
||||
} yield ListVettedPackagesResult(context, item)
|
||||
}
|
||||
}
|
||||
|
||||
final case class ListDomainParametersChangeResult(
|
||||
context: BaseResult,
|
||||
item: DynamicDomainParameters,
|
||||
)
|
||||
|
||||
object ListDomainParametersChangeResult {
|
||||
def fromProtoV0(
|
||||
value: v0.ListDomainParametersChangesResult.Result
|
||||
): ParsingResult[ListDomainParametersChangeResult] = for {
|
||||
contextP <- value.context.toRight(ProtoDeserializationError.FieldNotSet("context"))
|
||||
context <- BaseResult.fromProtoV0(contextP)
|
||||
dynamicDomainParametersInternal <- value.parameters match {
|
||||
case Parameters.Empty => Left(ProtoDeserializationError.FieldNotSet("parameters"))
|
||||
case Parameters.V1(ddpX) => DynamicDomainParametersInternal.fromProtoV2(ddpX)
|
||||
}
|
||||
item = DynamicDomainParameters(dynamicDomainParametersInternal)
|
||||
} yield ListDomainParametersChangeResult(context, item)
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.daml.ledger.api.v1.admin.user_management_service.Right.Kind
|
||||
import com.daml.ledger.api.v1.admin.user_management_service.{
|
||||
ListUsersResponse as ProtoListUsersResponse,
|
||||
Right as ProtoUserRight,
|
||||
User as ProtoLedgerApiUser,
|
||||
}
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.topology.PartyId
|
||||
import com.digitalasset.canton.{LfPartyId, ProtoDeserializationError}
|
||||
|
||||
import scala.util.control.NoStackTrace
|
||||
|
||||
final case class LedgerApiUser(
|
||||
id: String,
|
||||
primaryParty: Option[PartyId],
|
||||
isDeactivated: Boolean,
|
||||
metadata: LedgerApiObjectMeta,
|
||||
identityProviderId: String,
|
||||
)
|
||||
|
||||
object LedgerApiUser {
|
||||
def fromProtoV0(
|
||||
value: ProtoLedgerApiUser
|
||||
): ParsingResult[LedgerApiUser] = {
|
||||
val ProtoLedgerApiUser(id, primaryParty, isDeactivated, metadataO, identityProviderId) = value
|
||||
Option
|
||||
.when(primaryParty.nonEmpty)(primaryParty)
|
||||
.traverse(LfPartyId.fromString(_).flatMap(PartyId.fromLfParty(_)))
|
||||
.leftMap { err =>
|
||||
ProtoDeserializationError.ValueConversionError("primaryParty", err)
|
||||
}
|
||||
.map { primaryPartyO =>
|
||||
LedgerApiUser(
|
||||
id = id,
|
||||
primaryParty = primaryPartyO,
|
||||
isDeactivated = isDeactivated,
|
||||
metadata = LedgerApiObjectMeta(
|
||||
resourceVersion = metadataO.fold("")(_.resourceVersion),
|
||||
annotations = metadataO.fold(Map.empty[String, String])(_.annotations),
|
||||
),
|
||||
identityProviderId = identityProviderId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final case class UserRights(
|
||||
actAs: Set[PartyId],
|
||||
readAs: Set[PartyId],
|
||||
participantAdmin: Boolean,
|
||||
identityProviderAdmin: Boolean,
|
||||
)
|
||||
object UserRights {
|
||||
def fromProtoV0(
|
||||
values: Seq[ProtoUserRight]
|
||||
): ParsingResult[UserRights] = {
|
||||
Right(values.map(_.kind).foldLeft(UserRights(Set(), Set(), false, false)) {
|
||||
case (acc, Kind.Empty) => acc
|
||||
case (acc, Kind.ParticipantAdmin(value)) => acc.copy(participantAdmin = true)
|
||||
case (acc, Kind.CanActAs(value)) =>
|
||||
acc.copy(actAs = acc.actAs + PartyId.tryFromProtoPrimitive(value.party))
|
||||
case (acc, Kind.CanReadAs(value)) =>
|
||||
acc.copy(readAs = acc.readAs + PartyId.tryFromProtoPrimitive(value.party))
|
||||
case (acc, Kind.IdentityProviderAdmin(value)) =>
|
||||
acc.copy(identityProviderAdmin = true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
final case class ListLedgerApiUsersResult(users: Seq[LedgerApiUser], nextPageToken: String)
|
||||
|
||||
object ListLedgerApiUsersResult {
|
||||
def fromProtoV0(
|
||||
value: ProtoListUsersResponse,
|
||||
filterUser: String,
|
||||
): ParsingResult[ListLedgerApiUsersResult] = {
|
||||
val ProtoListUsersResponse(protoUsers, nextPageToken) = value
|
||||
protoUsers.traverse(LedgerApiUser.fromProtoV0).map { users =>
|
||||
ListLedgerApiUsersResult(users.filter(_.id.startsWith(filterUser)), nextPageToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents a user value exposed in the Canton console
|
||||
*/
|
||||
final case class User(
|
||||
id: String,
|
||||
primaryParty: Option[PartyId],
|
||||
isActive: Boolean,
|
||||
annotations: Map[String, String],
|
||||
identityProviderId: String,
|
||||
)
|
||||
|
||||
object User {
|
||||
def fromLapiUser(u: LedgerApiUser): User = User(
|
||||
id = u.id,
|
||||
primaryParty = u.primaryParty,
|
||||
isActive = !u.isDeactivated,
|
||||
annotations = u.metadata.annotations,
|
||||
identityProviderId = u.identityProviderId,
|
||||
)
|
||||
def toLapiUser(u: User, resourceVersion: Option[String]): LedgerApiUser = LedgerApiUser(
|
||||
id = u.id,
|
||||
primaryParty = u.primaryParty,
|
||||
isDeactivated = !u.isActive,
|
||||
metadata = LedgerApiObjectMeta(
|
||||
resourceVersion = resourceVersion.getOrElse(""),
|
||||
annotations = u.annotations,
|
||||
),
|
||||
identityProviderId = u.identityProviderId,
|
||||
)
|
||||
}
|
||||
|
||||
final case class ModifyingNonModifiableUserPropertiesError()
|
||||
extends RuntimeException("MODIFYING_AN_UNMODIFIABLE_USER_PROPERTY_ERROR")
|
||||
with NoStackTrace
|
||||
|
||||
final case class UsersPage(users: Seq[User], nextPageToken: String)
|
@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data.crypto
|
||||
|
||||
sealed trait CryptoKeyFormat extends Product with Serializable {
|
||||
def name: String
|
||||
override def toString: String = name
|
||||
}
|
||||
|
||||
object CryptoKeyFormat {
|
||||
case object Tink extends CryptoKeyFormat {
|
||||
override val name: String = "Tink"
|
||||
}
|
||||
|
||||
case object Der extends CryptoKeyFormat {
|
||||
override val name: String = "DER"
|
||||
}
|
||||
|
||||
case object Raw extends CryptoKeyFormat {
|
||||
override val name: String = "Raw"
|
||||
}
|
||||
|
||||
case object Symbolic extends CryptoKeyFormat {
|
||||
override val name: String = "Symbolic"
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data.crypto
|
||||
|
||||
/** Key schemes for asymmetric/hybrid encryption. */
|
||||
sealed trait EncryptionKeyScheme extends Product with Serializable {
|
||||
def name: String
|
||||
override def toString: String = name
|
||||
}
|
||||
|
||||
object EncryptionKeyScheme {
|
||||
case object EciesP256HkdfHmacSha256Aes128Gcm extends EncryptionKeyScheme {
|
||||
override val name: String = "ECIES-P256_HMAC256_AES128-GCM"
|
||||
}
|
||||
|
||||
case object EciesP256HmacSha256Aes128Cbc extends EncryptionKeyScheme {
|
||||
override val name: String = "ECIES-P256_HMAC256_AES128-CBC"
|
||||
}
|
||||
|
||||
case object Rsa2048OaepSha256 extends EncryptionKeyScheme {
|
||||
override val name: String = "RSA2048-OAEP-SHA256"
|
||||
}
|
||||
}
|
||||
|
||||
/** Key schemes for symmetric encryption. */
|
||||
sealed trait SymmetricKeyScheme extends Product with Serializable {
|
||||
def name: String
|
||||
override def toString: String = name
|
||||
|
||||
def keySizeInBytes: Int
|
||||
}
|
||||
|
||||
object SymmetricKeyScheme {
|
||||
|
||||
/** AES with 128bit key in GCM */
|
||||
case object Aes128Gcm extends SymmetricKeyScheme {
|
||||
override def name: String = "AES128-GCM"
|
||||
override def keySizeInBytes: Int = 16
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data.crypto
|
||||
|
||||
sealed abstract class HashAlgorithm(val name: String)
|
||||
|
||||
object HashAlgorithm {
|
||||
case object Sha256 extends HashAlgorithm("SHA-256")
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data.crypto
|
||||
|
||||
sealed trait SigningKeyScheme extends Product with Serializable {
|
||||
def name: String
|
||||
|
||||
override def toString: String = name
|
||||
}
|
||||
|
||||
/** Schemes for signature keys.
|
||||
*
|
||||
* Ed25519 is the best performing curve and should be the default.
|
||||
* EC-DSA is slower than Ed25519 but has better compatibility with other systems (such as CCF).
|
||||
*/
|
||||
object SigningKeyScheme {
|
||||
case object Ed25519 extends SigningKeyScheme {
|
||||
override val name: String = "Ed25519"
|
||||
}
|
||||
|
||||
case object EcDsaP256 extends SigningKeyScheme {
|
||||
override def name: String = "ECDSA-P256"
|
||||
}
|
||||
|
||||
case object EcDsaP384 extends SigningKeyScheme {
|
||||
override def name: String = "ECDSA-P384"
|
||||
}
|
||||
}
|
@ -0,0 +1,325 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.admin.api.client.data.topologyx
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.daml.nonempty.NonEmpty
|
||||
import com.digitalasset.canton.ProtoDeserializationError.RefinedDurationConversionError
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.crypto.Fingerprint
|
||||
import com.digitalasset.canton.protocol.DynamicDomainParameters
|
||||
import com.digitalasset.canton.serialization.ProtoConverter
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.topology.admin.v1
|
||||
import com.digitalasset.canton.topology.transaction.{
|
||||
AuthorityOfX,
|
||||
DomainTrustCertificateX,
|
||||
IdentifierDelegationX,
|
||||
MediatorDomainStateX,
|
||||
NamespaceDelegationX,
|
||||
OwnerToKeyMappingX,
|
||||
ParticipantDomainPermissionX,
|
||||
PartyHostingLimitsX,
|
||||
PartyToParticipantX,
|
||||
PurgeTopologyTransactionX,
|
||||
SequencerDomainStateX,
|
||||
TopologyChangeOpX,
|
||||
TrafficControlStateX,
|
||||
UnionspaceDefinitionX,
|
||||
VettedPackagesX,
|
||||
}
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
final case class BaseResult(
|
||||
domain: String,
|
||||
validFrom: Instant,
|
||||
validUntil: Option[Instant],
|
||||
operation: TopologyChangeOpX,
|
||||
transactionHash: ByteString,
|
||||
serial: PositiveInt,
|
||||
signedBy: NonEmpty[Seq[Fingerprint]],
|
||||
)
|
||||
|
||||
object BaseResult {
|
||||
def fromProtoV1(value: v1.BaseResult): ParsingResult[BaseResult] =
|
||||
for {
|
||||
protoValidFrom <- ProtoConverter.required("valid_from", value.validFrom)
|
||||
validFrom <- ProtoConverter.InstantConverter.fromProtoPrimitive(protoValidFrom)
|
||||
validUntil <- value.validUntil.traverse(ProtoConverter.InstantConverter.fromProtoPrimitive)
|
||||
operation <- TopologyChangeOpX.fromProtoV2(value.operation)
|
||||
serial <- PositiveInt
|
||||
.create(value.serial)
|
||||
.leftMap(e => RefinedDurationConversionError("serial", e.message))
|
||||
signedBy <-
|
||||
ProtoConverter.parseRequiredNonEmpty(
|
||||
Fingerprint.fromProtoPrimitive,
|
||||
"signed_by_fingerprints",
|
||||
value.signedByFingerprints,
|
||||
)
|
||||
} yield BaseResult(
|
||||
value.store,
|
||||
validFrom,
|
||||
validUntil,
|
||||
operation,
|
||||
value.transactionHash,
|
||||
serial,
|
||||
signedBy,
|
||||
)
|
||||
}
|
||||
|
||||
final case class ListNamespaceDelegationResult(
|
||||
context: BaseResult,
|
||||
item: NamespaceDelegationX,
|
||||
)
|
||||
|
||||
object ListNamespaceDelegationResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListNamespaceDelegationResult.Result
|
||||
): ParsingResult[ListNamespaceDelegationResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- NamespaceDelegationX.fromProtoV2(itemProto)
|
||||
} yield ListNamespaceDelegationResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListUnionspaceDefinitionResult(
|
||||
context: BaseResult,
|
||||
item: UnionspaceDefinitionX,
|
||||
)
|
||||
|
||||
object ListUnionspaceDefinitionResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListUnionspaceDefinitionResult.Result
|
||||
): ParsingResult[ListUnionspaceDefinitionResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- UnionspaceDefinitionX.fromProtoV2(itemProto)
|
||||
} yield ListUnionspaceDefinitionResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListIdentifierDelegationResult(
|
||||
context: BaseResult,
|
||||
item: IdentifierDelegationX,
|
||||
)
|
||||
|
||||
object ListIdentifierDelegationResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListIdentifierDelegationResult.Result
|
||||
): ParsingResult[ListIdentifierDelegationResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- IdentifierDelegationX.fromProtoV2(itemProto)
|
||||
} yield ListIdentifierDelegationResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListOwnerToKeyMappingResult(
|
||||
context: BaseResult,
|
||||
item: OwnerToKeyMappingX,
|
||||
)
|
||||
|
||||
object ListOwnerToKeyMappingResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListOwnerToKeyMappingResult.Result
|
||||
): ParsingResult[ListOwnerToKeyMappingResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- OwnerToKeyMappingX.fromProtoV2(itemProto)
|
||||
} yield ListOwnerToKeyMappingResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListDomainTrustCertificateResult(
|
||||
context: BaseResult,
|
||||
item: DomainTrustCertificateX,
|
||||
)
|
||||
|
||||
object ListDomainTrustCertificateResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListDomainTrustCertificateResult.Result
|
||||
): ParsingResult[ListDomainTrustCertificateResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- DomainTrustCertificateX.fromProtoV2(itemProto)
|
||||
} yield ListDomainTrustCertificateResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListParticipantDomainPermissionResult(
|
||||
context: BaseResult,
|
||||
item: ParticipantDomainPermissionX,
|
||||
)
|
||||
|
||||
object ListParticipantDomainPermissionResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListParticipantDomainPermissionResult.Result
|
||||
): ParsingResult[ListParticipantDomainPermissionResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- ParticipantDomainPermissionX.fromProtoV2(itemProto)
|
||||
} yield ListParticipantDomainPermissionResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListPartyHostingLimitsResult(
|
||||
context: BaseResult,
|
||||
item: PartyHostingLimitsX,
|
||||
)
|
||||
|
||||
object ListPartyHostingLimitsResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListPartyHostingLimitsResult.Result
|
||||
): ParsingResult[ListPartyHostingLimitsResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- PartyHostingLimitsX.fromProtoV2(itemProto)
|
||||
} yield ListPartyHostingLimitsResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListVettedPackagesResult(
|
||||
context: BaseResult,
|
||||
item: VettedPackagesX,
|
||||
)
|
||||
|
||||
object ListVettedPackagesResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListVettedPackagesResult.Result
|
||||
): ParsingResult[ListVettedPackagesResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- VettedPackagesX.fromProtoV2(itemProto)
|
||||
} yield ListVettedPackagesResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListPartyToParticipantResult(
|
||||
context: BaseResult,
|
||||
item: PartyToParticipantX,
|
||||
)
|
||||
|
||||
object ListPartyToParticipantResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListPartyToParticipantResult.Result
|
||||
): ParsingResult[ListPartyToParticipantResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- PartyToParticipantX.fromProtoV2(itemProto)
|
||||
} yield ListPartyToParticipantResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListAuthorityOfResult(
|
||||
context: BaseResult,
|
||||
item: AuthorityOfX,
|
||||
)
|
||||
|
||||
object ListAuthorityOfResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListAuthorityOfResult.Result
|
||||
): ParsingResult[ListAuthorityOfResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- AuthorityOfX.fromProtoV2(itemProto)
|
||||
} yield ListAuthorityOfResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListDomainParametersStateResult(
|
||||
context: BaseResult,
|
||||
item: DynamicDomainParameters,
|
||||
)
|
||||
|
||||
object ListDomainParametersStateResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListDomainParametersStateResult.Result
|
||||
): ParsingResult[ListDomainParametersStateResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- DynamicDomainParameters.fromProtoV2(itemProto)
|
||||
} yield ListDomainParametersStateResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListMediatorDomainStateResult(
|
||||
context: BaseResult,
|
||||
item: MediatorDomainStateX,
|
||||
)
|
||||
|
||||
object ListMediatorDomainStateResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListMediatorDomainStateResult.Result
|
||||
): ParsingResult[ListMediatorDomainStateResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- MediatorDomainStateX.fromProtoV2(itemProto)
|
||||
} yield ListMediatorDomainStateResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListSequencerDomainStateResult(
|
||||
context: BaseResult,
|
||||
item: SequencerDomainStateX,
|
||||
)
|
||||
|
||||
object ListSequencerDomainStateResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListSequencerDomainStateResult.Result
|
||||
): ParsingResult[ListSequencerDomainStateResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- SequencerDomainStateX.fromProtoV2(itemProto)
|
||||
} yield ListSequencerDomainStateResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListPurgeTopologyTransactionXResult(
|
||||
context: BaseResult,
|
||||
item: PurgeTopologyTransactionX,
|
||||
)
|
||||
|
||||
object ListPurgeTopologyTransactionXResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListPurgeTopologyTransactionXResult.Result
|
||||
): ParsingResult[ListPurgeTopologyTransactionXResult] =
|
||||
for {
|
||||
contextProto <- ProtoConverter.required("context", value.context)
|
||||
context <- BaseResult.fromProtoV1(contextProto)
|
||||
itemProto <- ProtoConverter.required("item", value.item)
|
||||
item <- PurgeTopologyTransactionX.fromProtoV2(itemProto)
|
||||
} yield ListPurgeTopologyTransactionXResult(context, item)
|
||||
}
|
||||
|
||||
final case class ListTrafficStateResult(
|
||||
context: BaseResult,
|
||||
item: TrafficControlStateX,
|
||||
)
|
||||
|
||||
object ListTrafficStateResult {
|
||||
def fromProtoV1(
|
||||
value: v1.ListTrafficStateResult.Result
|
||||
): ParsingResult[ListTrafficStateResult] =
|
||||
for {
|
||||
context <- ProtoConverter.parseRequired(BaseResult.fromProtoV1, "context", value.context)
|
||||
item <- ProtoConverter.parseRequired(TrafficControlStateX.fromProtoV2, "item", value.item)
|
||||
} yield ListTrafficStateResult(context, item)
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.config
|
||||
|
||||
import cats.data.Validated
|
||||
import cats.syntax.functor.*
|
||||
import com.daml.nonempty.NonEmpty
|
||||
import com.digitalasset.canton.config.CantonRequireTypes.InstanceName
|
||||
import com.digitalasset.canton.config.ConfigErrors.CantonConfigError
|
||||
import com.digitalasset.canton.domain.config.{
|
||||
CommunityDomainConfig,
|
||||
DomainBaseConfig,
|
||||
RemoteDomainConfig,
|
||||
}
|
||||
import com.digitalasset.canton.logging.{ErrorLoggingContext, NamedLoggerFactory, TracedLogger}
|
||||
import com.digitalasset.canton.participant.config.{
|
||||
CommunityParticipantConfig,
|
||||
LocalParticipantConfig,
|
||||
RemoteParticipantConfig,
|
||||
}
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.typesafe.config.Config
|
||||
import monocle.macros.syntax.lens.*
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import pureconfig.{ConfigReader, ConfigWriter}
|
||||
|
||||
import java.io.File
|
||||
import scala.annotation.nowarn
|
||||
|
||||
final case class CantonCommunityConfig(
|
||||
domains: Map[InstanceName, CommunityDomainConfig] = Map.empty,
|
||||
participants: Map[InstanceName, CommunityParticipantConfig] = Map.empty,
|
||||
participantsX: Map[InstanceName, CommunityParticipantConfig] = Map.empty,
|
||||
remoteDomains: Map[InstanceName, RemoteDomainConfig] = Map.empty,
|
||||
remoteParticipants: Map[InstanceName, RemoteParticipantConfig] = Map.empty,
|
||||
remoteParticipantsX: Map[InstanceName, RemoteParticipantConfig] = Map.empty,
|
||||
monitoring: MonitoringConfig = MonitoringConfig(),
|
||||
parameters: CantonParameters = CantonParameters(),
|
||||
features: CantonFeatures = CantonFeatures(),
|
||||
) extends CantonConfig
|
||||
with ConfigDefaults[DefaultPorts, CantonCommunityConfig] {
|
||||
|
||||
override type DomainConfigType = CommunityDomainConfig
|
||||
override type ParticipantConfigType = CommunityParticipantConfig
|
||||
|
||||
/** renders the config as json (used for dumping config for diagnostic purposes) */
|
||||
override def dumpString: String = CantonCommunityConfig.makeConfidentialString(this)
|
||||
|
||||
override def validate: Validated[NonEmpty[Seq[String]], Unit] =
|
||||
CommunityConfigValidations.validate(this)
|
||||
|
||||
override def withDefaults(ports: DefaultPorts): CantonCommunityConfig =
|
||||
this
|
||||
.focus(_.domains)
|
||||
.modify(_.fmap(_.withDefaults(ports)))
|
||||
.focus(_.participants)
|
||||
.modify(_.fmap(_.withDefaults(ports)))
|
||||
.focus(_.participantsX)
|
||||
.modify(_.fmap(_.withDefaults(ports)))
|
||||
}
|
||||
|
||||
@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072
|
||||
object CantonCommunityConfig {
|
||||
|
||||
/** Combine together deprecated implicits for types that define them
|
||||
* This setup allows the compiler to pick the implicit for the most specific type when applying deprecations.
|
||||
* For instance,
|
||||
* ConfigReader[LocalParticipantConfig].applyDeprecations will pick up the deprecations implicit defined in
|
||||
* LocalParticipantConfig instead of LocalNodeConfig
|
||||
* despite LocalParticipantConfig being a subtype of LocalNodeConfig.
|
||||
*/
|
||||
object CantonDeprecationImplicits
|
||||
extends LocalNodeConfig.LocalNodeConfigDeprecationImplicits
|
||||
with LocalParticipantConfig.LocalParticipantDeprecationsImplicits
|
||||
with DomainBaseConfig.DomainBaseConfigDeprecationImplicits
|
||||
|
||||
private val logger: Logger = LoggerFactory.getLogger(classOf[CantonCommunityConfig])
|
||||
private val elc = ErrorLoggingContext(
|
||||
TracedLogger(logger),
|
||||
NamedLoggerFactory.root.properties,
|
||||
TraceContext.empty,
|
||||
)
|
||||
import pureconfig.generic.semiauto.*
|
||||
import CantonConfig.*
|
||||
|
||||
// Implemented as a def so we can pass the ErrorLoggingContext to be used during parsing
|
||||
@nowarn("cat=unused")
|
||||
private implicit def cantonCommunityConfigReader(implicit
|
||||
elc: ErrorLoggingContext
|
||||
): ConfigReader[CantonCommunityConfig] = { // memoize it so we get the same instance every time
|
||||
val configReaders: ConfigReaders = new ConfigReaders()
|
||||
import configReaders.*
|
||||
import DeprecatedConfigUtils.*
|
||||
import CantonDeprecationImplicits.*
|
||||
|
||||
implicit val communityDomainConfigReader: ConfigReader[CommunityDomainConfig] =
|
||||
deriveReader[CommunityDomainConfig].applyDeprecations
|
||||
implicit val communityParticipantConfigReader: ConfigReader[CommunityParticipantConfig] =
|
||||
deriveReader[CommunityParticipantConfig].applyDeprecations
|
||||
|
||||
deriveReader[CantonCommunityConfig]
|
||||
}
|
||||
|
||||
@nowarn("cat=unused")
|
||||
private lazy implicit val cantonCommunityConfigWriter: ConfigWriter[CantonCommunityConfig] = {
|
||||
val writers = new CantonConfig.ConfigWriters(confidential = true)
|
||||
import writers.*
|
||||
implicit val communityDomainConfigWriter: ConfigWriter[CommunityDomainConfig] =
|
||||
deriveWriter[CommunityDomainConfig]
|
||||
implicit val communityParticipantConfigWriter: ConfigWriter[CommunityParticipantConfig] =
|
||||
deriveWriter[CommunityParticipantConfig]
|
||||
|
||||
deriveWriter[CantonCommunityConfig]
|
||||
}
|
||||
|
||||
def load(config: Config)(implicit
|
||||
elc: ErrorLoggingContext = elc
|
||||
): Either[CantonConfigError, CantonCommunityConfig] =
|
||||
CantonConfig.loadAndValidate[CantonCommunityConfig](config)
|
||||
|
||||
def loadOrExit(config: Config)(implicit elc: ErrorLoggingContext = elc): CantonCommunityConfig =
|
||||
CantonConfig.loadOrExit[CantonCommunityConfig](config)
|
||||
|
||||
def parseAndLoad(files: Seq[File])(implicit
|
||||
elc: ErrorLoggingContext = elc
|
||||
): Either[CantonConfigError, CantonCommunityConfig] =
|
||||
CantonConfig.parseAndLoad[CantonCommunityConfig](files)
|
||||
|
||||
def parseAndLoadOrExit(files: Seq[File])(implicit
|
||||
elc: ErrorLoggingContext = elc
|
||||
): CantonCommunityConfig =
|
||||
CantonConfig.parseAndLoadOrExit[CantonCommunityConfig](files)
|
||||
|
||||
def makeConfidentialString(config: CantonCommunityConfig): String =
|
||||
"canton " + ConfigWriter[CantonCommunityConfig]
|
||||
.to(config)
|
||||
.render(CantonConfig.defaultConfigRenderer)
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,302 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.config
|
||||
|
||||
import cats.data.Validated
|
||||
import cats.syntax.foldable.*
|
||||
import cats.syntax.functor.*
|
||||
import cats.syntax.functorFilter.*
|
||||
import com.daml.nonempty.NonEmpty
|
||||
import com.daml.nonempty.catsinstances.*
|
||||
import com.digitalasset.canton.config.CantonRequireTypes.InstanceName
|
||||
import com.digitalasset.canton.domain.config.DomainParametersConfig
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.digitalasset.canton.version.HandshakeErrors.DeprecatedProtocolVersion
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
|
||||
import java.net.URI
|
||||
|
||||
private[config] trait ConfigValidations[C <: CantonConfig] {
|
||||
final def validate(config: C): Validated[NonEmpty[Seq[String]], Unit] =
|
||||
validations.traverse_(_(config))
|
||||
|
||||
protected val validations: List[C => Validated[NonEmpty[Seq[String]], Unit]]
|
||||
}
|
||||
|
||||
object CommunityConfigValidations
|
||||
extends ConfigValidations[CantonCommunityConfig]
|
||||
with NamedLogging {
|
||||
import TraceContext.Implicits.Empty.*
|
||||
override protected def loggerFactory: NamedLoggerFactory = NamedLoggerFactory.root
|
||||
|
||||
final case class DbAccess(url: String, user: Option[String]) {
|
||||
private lazy val urlNoPassword = {
|
||||
val uri = new URI(
|
||||
url.replace("jdbc:", "")
|
||||
)
|
||||
val queryNoPassword = Option(uri.getQuery)
|
||||
.getOrElse("")
|
||||
.split('&')
|
||||
.map(param =>
|
||||
if (param.startsWith("password=")) ""
|
||||
else param
|
||||
)
|
||||
.mkString
|
||||
new URI(uri.getScheme, uri.getAuthority, uri.getPath, queryNoPassword, uri.getFragment)
|
||||
}
|
||||
|
||||
override def toString: String =
|
||||
s"DbAccess($urlNoPassword, $user)"
|
||||
}
|
||||
|
||||
type Validation = CantonCommunityConfig => Validated[NonEmpty[Seq[String]], Unit]
|
||||
|
||||
override protected val validations: List[Validation] =
|
||||
List[Validation](noDuplicateStorage, atLeastOneNode) ++ genericValidations[
|
||||
CantonCommunityConfig
|
||||
]
|
||||
|
||||
/** Validations applied to all community and enterprise Canton configurations. */
|
||||
private[config] def genericValidations[C <: CantonConfig]
|
||||
: List[C => Validated[NonEmpty[Seq[String]], Unit]] =
|
||||
List(
|
||||
developmentProtocolSafetyCheckDomains,
|
||||
developmentProtocolSafetyCheckParticipants,
|
||||
warnIfUnsafeMinProtocolVersion,
|
||||
warnIfUnsafeProtocolVersionEmbeddedDomain,
|
||||
adminTokenSafetyCheckParticipants,
|
||||
)
|
||||
|
||||
/** Group node configs by db access to find matching db storage configs.
|
||||
* Overcomplicated types used are to work around that at this point nodes could have conflicting names so we can't just
|
||||
* throw them all in a single map.
|
||||
*/
|
||||
private[config] def extractNormalizedDbAccess[C <: CantonConfig](
|
||||
nodeConfigs: Map[String, LocalNodeConfig]*
|
||||
): Map[DbAccess, List[(String, LocalNodeConfig)]] = {
|
||||
// Basic attempt to normalize JDBC URL-based configuration and explicit property configuration
|
||||
// Limitations: Does not parse nor normalize the JDBC URLs
|
||||
def normalize(dbConfig: DbConfig): Option[DbAccess] = {
|
||||
import slick.util.ConfigExtensionMethods.*
|
||||
|
||||
val slickConfig = dbConfig.config
|
||||
|
||||
def getPropStr(prop: String): Option[String] =
|
||||
slickConfig.getStringOpt(prop).orElse(slickConfig.getStringOpt(s"properties.$prop"))
|
||||
|
||||
def getPropInt(prop: String): Option[Int] =
|
||||
slickConfig.getIntOpt(prop).orElse(slickConfig.getIntOpt(s"properties.$prop"))
|
||||
|
||||
def extractUrl: Option[String] =
|
||||
getPropStr("url").orElse(getPropStr("jdbcUrl"))
|
||||
|
||||
def extractServerPortDbAsUrl: Option[String] =
|
||||
for {
|
||||
server <- getPropStr("serverName")
|
||||
port <- getPropInt("portNumber")
|
||||
dbName <- getPropStr("databaseName")
|
||||
url = dbConfig match {
|
||||
case _: H2DbConfig => DbConfig.h2Url(dbName)
|
||||
case _: PostgresDbConfig => DbConfig.postgresUrl(server, port, dbName)
|
||||
// Assume Oracle
|
||||
case _ => DbConfig.oracleUrl(server, port, dbName)
|
||||
}
|
||||
} yield url
|
||||
|
||||
val user = getPropStr("user")
|
||||
extractUrl.orElse(extractServerPortDbAsUrl).map(url => DbAccess(url = url, user = user))
|
||||
}
|
||||
|
||||
// combine into a single list of name to config
|
||||
val configs = nodeConfigs.map(_.toList).foldLeft(List[(String, LocalNodeConfig)]())(_ ++ _)
|
||||
|
||||
val withStorageConfigs = configs.mapFilter { case (name, config) =>
|
||||
config.storage match {
|
||||
case dbConfig: DbConfig => normalize(dbConfig).map((_, name, config))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
withStorageConfigs
|
||||
.groupBy { case (dbAccess, _, _) => dbAccess }
|
||||
.fmap(_.map { case (_, name, config) =>
|
||||
(name, config)
|
||||
})
|
||||
}
|
||||
|
||||
private[config] def formatNodeList(nodes: List[(String, LocalNodeConfig)]): String =
|
||||
nodes.map { case (name, config) => s"${config.nodeTypeName} $name" }.mkString(",")
|
||||
|
||||
/** Validate the config that the storage configuration is not shared between nodes. */
|
||||
private def noDuplicateStorage(
|
||||
config: CantonCommunityConfig
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
val dbAccessToNodes =
|
||||
extractNormalizedDbAccess(config.participantsByString, config.domainsByString)
|
||||
|
||||
dbAccessToNodes.toSeq
|
||||
.traverse_ {
|
||||
case (dbAccess, nodes) if nodes.lengthCompare(1) > 0 =>
|
||||
Validated.invalid(
|
||||
NonEmpty(Seq, s"Nodes ${formatNodeList(nodes)} share same DB access: $dbAccess")
|
||||
)
|
||||
case _ => Validated.valid(())
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Product", "org.wartremover.warts.Serializable"))
|
||||
private def atLeastOneNode(
|
||||
config: CantonCommunityConfig
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
val CantonCommunityConfig(
|
||||
domains,
|
||||
participants,
|
||||
participantsX,
|
||||
remoteDomains,
|
||||
remoteParticipants,
|
||||
remoteParticipantsX,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
) =
|
||||
config
|
||||
Validated.cond(
|
||||
Seq(
|
||||
domains,
|
||||
participants,
|
||||
remoteDomains,
|
||||
remoteParticipants,
|
||||
participantsX,
|
||||
remoteParticipantsX,
|
||||
)
|
||||
.exists(_.nonEmpty),
|
||||
(),
|
||||
NonEmpty(Seq, "At least one node must be defined in the configuration"),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private[config] val backwardsCompatibleLoggingConfigErr =
|
||||
"Inconsistent configuration of canton.monitoring.log-message-payloads and canton.monitoring.logging.api.message-payloads. Please use the latter in your configuration"
|
||||
|
||||
private def developmentProtocolSafetyCheckDomains(
|
||||
config: CantonConfig
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
developmentProtocolSafetyCheck(
|
||||
config.parameters.nonStandardConfig,
|
||||
config.domains.toSeq.map { case (k, v) =>
|
||||
(k, v.init.domainParameters)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private def developmentProtocolSafetyCheckParticipants(
|
||||
config: CantonConfig
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
def toNe(
|
||||
name: String,
|
||||
nonStandardConfig: Boolean,
|
||||
devVersionSupport: Boolean,
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
Validated.cond(
|
||||
nonStandardConfig || !devVersionSupport,
|
||||
(),
|
||||
NonEmpty(
|
||||
Seq,
|
||||
s"Enabling dev-version-support for participant $name requires you to explicitly set canton.parameters.non-standard-config = yes",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
config.participants.toList.traverse_ { case (name, participantConfig) =>
|
||||
toNe(
|
||||
name.unwrap,
|
||||
config.parameters.nonStandardConfig,
|
||||
participantConfig.parameters.devVersionSupport,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def warnIfUnsafeMinProtocolVersion(
|
||||
config: CantonConfig
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
config.participants.toSeq.foreach { case (name, config) =>
|
||||
val minimum = config.parameters.minimumProtocolVersion.map(_.unwrap)
|
||||
val isMinimumDeprecatedVersion = minimum.getOrElse(ProtocolVersion.minimum).isDeprecated
|
||||
|
||||
if (isMinimumDeprecatedVersion && !config.parameters.dontWarnOnDeprecatedPV)
|
||||
DeprecatedProtocolVersion.WarnParticipant(name, minimum).discard
|
||||
}
|
||||
Validated.valid(())
|
||||
}
|
||||
|
||||
private def warnIfUnsafeProtocolVersionEmbeddedDomain(
|
||||
config: CantonConfig
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
config.domains.toSeq.foreach { case (name, config) =>
|
||||
val pv = config.init.domainParameters.protocolVersion.unwrap
|
||||
if (pv.isDeprecated && !config.init.domainParameters.dontWarnOnDeprecatedPV)
|
||||
DeprecatedProtocolVersion.WarnDomain(name, pv).discard
|
||||
}
|
||||
Validated.valid(())
|
||||
}
|
||||
|
||||
private[config] def developmentProtocolSafetyCheck(
|
||||
allowUnstableProtocolVersion: Boolean,
|
||||
namesAndConfig: Seq[(InstanceName, DomainParametersConfig)],
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
def toNe(
|
||||
name: String,
|
||||
protocolVersion: ProtocolVersion,
|
||||
allowUnstableProtocolVersion: Boolean,
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
Validated.cond(
|
||||
protocolVersion.isStable || allowUnstableProtocolVersion,
|
||||
(),
|
||||
NonEmpty(
|
||||
Seq,
|
||||
s"Using non-stable protocol $protocolVersion for node $name requires you to explicitly set canton.parameters.non-standard-config = yes",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
namesAndConfig.toList.traverse_ { case (name, parameters) =>
|
||||
toNe(
|
||||
name.unwrap,
|
||||
parameters.protocolVersion.version,
|
||||
allowUnstableProtocolVersion,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def adminTokenSafetyCheckParticipants(
|
||||
config: CantonConfig
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
def toNe(
|
||||
name: String,
|
||||
nonStandardConfig: Boolean,
|
||||
adminToken: Option[String],
|
||||
): Validated[NonEmpty[Seq[String]], Unit] = {
|
||||
Validated.cond(
|
||||
nonStandardConfig || adminToken.isEmpty,
|
||||
(),
|
||||
NonEmpty(
|
||||
Seq,
|
||||
s"Setting ledger-api.admin-token for participant $name requires you to explicitly set canton.parameters.non-standard-config = yes",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
config.participants.toList.traverse_ { case (name, participantConfig) =>
|
||||
toNe(
|
||||
name.unwrap,
|
||||
config.parameters.nonStandardConfig,
|
||||
participantConfig.ledgerApi.adminToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.config
|
||||
|
||||
import com.daml.error.{ErrorCategory, ErrorCode, Explanation, Resolution}
|
||||
import com.digitalasset.canton.error.CantonError
|
||||
import com.digitalasset.canton.error.CantonErrorGroups.ConfigErrorGroup
|
||||
import com.digitalasset.canton.logging.ErrorLoggingContext
|
||||
import com.typesafe.config.ConfigException
|
||||
import org.slf4j.event.Level
|
||||
import pureconfig.error.ConfigReaderFailures
|
||||
|
||||
import java.io.File
|
||||
import scala.collection.mutable
|
||||
import scala.reflect.{ClassTag, classTag}
|
||||
|
||||
/** * Trait which acts as a wrapper around
|
||||
* 1. `lightbend ConfigException`s which are caught when attempting to read or parse a configuration file
|
||||
* 2. `pureconfig ConfigReaderFailures` which are returned when attempting to convert a given
|
||||
* [[com.typesafe.config.Config]] instance (basically a valid HOCON-file)
|
||||
* to one of the Canton configs
|
||||
*/
|
||||
object ConfigErrors extends ConfigErrorGroup {
|
||||
|
||||
sealed abstract class ConfigErrorCode(id: String)
|
||||
extends ErrorCode(id, ErrorCategory.InvalidIndependentOfSystemState) {
|
||||
// we classify ConfigErrors as ERROR so they are shown directly to the user when he attempts to start Canton
|
||||
// via canton -c <config-file>
|
||||
override def logLevel: Level = Level.ERROR
|
||||
override def errorConveyanceDocString: Option[String] =
|
||||
Some(
|
||||
"Config errors are logged and output to stdout if starting Canton with a given configuration fails"
|
||||
)
|
||||
}
|
||||
|
||||
abstract class CantonConfigError(
|
||||
override val cause: String,
|
||||
override val throwableO: Option[Throwable] = None,
|
||||
)(implicit override val code: ErrorCode)
|
||||
extends CantonError {}
|
||||
|
||||
sealed abstract class ExceptionBasedConfigError(
|
||||
override val cause: String,
|
||||
override val throwableO: Option[Throwable] = None,
|
||||
)(implicit override val code: ErrorCode)
|
||||
extends CantonConfigError(cause, throwableO)(code) {
|
||||
|
||||
def exceptions: Seq[ConfigException]
|
||||
|
||||
override def log(): Unit = {
|
||||
super.log()
|
||||
|
||||
exceptions.foreach { e =>
|
||||
loggingContext.logger.debug(
|
||||
code.toMsg(
|
||||
s"Received the following exception while attempting to parse the Canton config files",
|
||||
loggingContext.traceContext.traceId,
|
||||
),
|
||||
e,
|
||||
)(loggingContext.traceContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final case object NoConfigFiles extends ConfigErrorCode("NO_CONFIG_FILES") {
|
||||
final case class Error()(implicit override val loggingContext: ErrorLoggingContext)
|
||||
extends CantonConfigError(
|
||||
"No config files were given to Canton. We require at least one config file given via --config or a key:value pair given via -C."
|
||||
)
|
||||
}
|
||||
|
||||
@Resolution(""" In general, this can be one of many errors since this is the 'miscellaneous category' of configuration errors.
|
||||
| One of the more common errors in this category is an 'unknown key' error. This error usually means that
|
||||
| a keyword that is not valid (e.g. it may have a typo 'bort' instead of 'port'), or that a valid keyword
|
||||
| at the wrong part of the configuration hierarchy was used (e.g. to enable database replication for a participant, the correct configuration
|
||||
| is `canton.participants.participant2.replication.enabled = true` and not `canton.participants.replication.enabled = true`).
|
||||
| Please refer to the scaladoc of either `CantonEnterpriseConfig` or `CantonCommunityConfig` (depending on whether the community or enterprise version is used) to find the valid configuration keywords and the correct position in the configuration hierarchy.
|
||||
|""")
|
||||
final case object GenericConfigError extends ConfigErrorCode("GENERIC_CONFIG_ERROR") {
|
||||
final case class Error(override val cause: String)(implicit
|
||||
override val loggingContext: ErrorLoggingContext
|
||||
) extends CantonConfigError(cause)
|
||||
}
|
||||
|
||||
@Explanation(
|
||||
"This error is usually thrown when Canton can't find a given configuration file."
|
||||
)
|
||||
@Resolution(
|
||||
"Make sure that the path and name of all configuration files is correctly specified. "
|
||||
)
|
||||
final case object CannotReadFilesError extends ConfigErrorCode("CANNOT_READ_CONFIG_FILES") {
|
||||
final case class Error(unreadableFiles: Seq[File])(implicit
|
||||
override val loggingContext: ErrorLoggingContext
|
||||
) extends CantonConfigError("At least one configuration file could not be read.")
|
||||
}
|
||||
|
||||
@Explanation(
|
||||
"This error is usually thrown because a config file doesn't contain configs in valid HOCON format. " +
|
||||
"The most common cause of an invalid HOCON format is a forgotten bracket."
|
||||
)
|
||||
@Resolution("Make sure that all files are in valid HOCON format.")
|
||||
final case object CannotParseFilesError extends ConfigErrorCode("CANNOT_PARSE_CONFIG_FILES") {
|
||||
final case class Error(override val exceptions: Seq[ConfigException])(implicit
|
||||
override val loggingContext: ErrorLoggingContext
|
||||
) extends ExceptionBasedConfigError(
|
||||
s"Received an exception (full stack trace has been logged at DEBUG level) while attempting to parse ${exceptions.length} .conf-file(s)."
|
||||
)
|
||||
}
|
||||
|
||||
@Resolution(
|
||||
"A common cause of this error is attempting to use an environment variable that isn't defined within a config-file. "
|
||||
)
|
||||
final case object SubstitutionError extends ConfigErrorCode("CONFIG_SUBSTITUTION_ERROR") {
|
||||
final case class Error(override val exceptions: Seq[ConfigException])(implicit
|
||||
override val loggingContext: ErrorLoggingContext
|
||||
) extends ExceptionBasedConfigError(
|
||||
s"Received an exception (full stack trace has been logged at DEBUG level) while attempting to parse ${exceptions.length} .conf-file(s)."
|
||||
)
|
||||
}
|
||||
|
||||
final case object ValidationError extends ConfigErrorCode("CONFIG_VALIDATION_ERROR") {
|
||||
final case class Error(causes: Seq[String])(implicit
|
||||
override val loggingContext: ErrorLoggingContext
|
||||
) extends CantonConfigError(
|
||||
s"Failed to validate the configuration due to: ${causes.mkString("\n")}"
|
||||
)
|
||||
}
|
||||
|
||||
def getMessage[ConfClass: ClassTag](failures: ConfigReaderFailures): String = {
|
||||
val linesBuffer = mutable.Buffer.empty[String]
|
||||
linesBuffer += s"Cannot convert configuration to a config of ${classTag[ConfClass].runtimeClass}. Failures are:"
|
||||
linesBuffer += failures.prettyPrint(1)
|
||||
linesBuffer += ""
|
||||
linesBuffer.mkString(System.lineSeparator())
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand
|
||||
import com.digitalasset.canton.concurrent.Threading
|
||||
import com.digitalasset.canton.config.{CantonConfig, NonNegativeDuration}
|
||||
import com.digitalasset.canton.console.CommandErrors.ConsoleTimeout
|
||||
import com.digitalasset.canton.crypto.Crypto
|
||||
import com.digitalasset.canton.environment.{CantonNode, CantonNodeBootstrap}
|
||||
import com.digitalasset.canton.logging.{NamedLogging, TracedLogger}
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/** Support for running an admin command
|
||||
*/
|
||||
trait AdminCommandRunner {
|
||||
|
||||
/** Run a GRPC admin command and return its result.
|
||||
* Most of the commands are only defined for the GRPC interface, so we default to showing an error message
|
||||
* if the command is called for a node configured with an HTTP interface.
|
||||
*/
|
||||
protected[console] def adminCommand[Result](
|
||||
grpcCommand: GrpcAdminCommand[_, _, Result]
|
||||
): ConsoleCommandResult[Result]
|
||||
|
||||
protected[console] def tracedLogger: TracedLogger
|
||||
|
||||
}
|
||||
|
||||
object AdminCommandRunner {
|
||||
def retryUntilTrue(timeout: NonNegativeDuration)(
|
||||
condition: => Boolean
|
||||
): ConsoleCommandResult[Unit] = {
|
||||
val deadline = timeout.asFiniteApproximation.fromNow
|
||||
@tailrec
|
||||
def go(): ConsoleCommandResult[Unit] = {
|
||||
val res = condition
|
||||
if (!res) {
|
||||
if (deadline.hasTimeLeft()) {
|
||||
Threading.sleep(100)
|
||||
go()
|
||||
} else {
|
||||
ConsoleTimeout.Error(timeout.asJavaApproximation)
|
||||
}
|
||||
} else {
|
||||
CommandSuccessful(())
|
||||
}
|
||||
}
|
||||
go()
|
||||
}
|
||||
}
|
||||
|
||||
/** Support for running ledgerApi commands
|
||||
*/
|
||||
trait LedgerApiCommandRunner {
|
||||
|
||||
protected[console] def ledgerApiCommand[Result](
|
||||
command: GrpcAdminCommand[_, _, Result]
|
||||
): ConsoleCommandResult[Result]
|
||||
|
||||
protected[console] def token: Option[String]
|
||||
|
||||
}
|
||||
|
||||
/** Support for inspecting the instance */
|
||||
trait BaseInspection[I <: CantonNode] {
|
||||
|
||||
def underlying: Option[I] = {
|
||||
runningNode.flatMap(_.getNode)
|
||||
}
|
||||
|
||||
protected[console] def runningNode: Option[CantonNodeBootstrap[I]]
|
||||
protected[console] def startingNode: Option[CantonNodeBootstrap[I]]
|
||||
protected[console] def name: String
|
||||
|
||||
protected[console] def access[T](ops: I => T): T = {
|
||||
ops(
|
||||
runningNode
|
||||
.getOrElse(throw new IllegalArgumentException(s"instance $name is not running"))
|
||||
.getNode
|
||||
.getOrElse(
|
||||
throw new IllegalArgumentException(
|
||||
s"instance $name is still starting or awaiting manual initialisation."
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
protected[canton] def crypto: Crypto = {
|
||||
runningNode
|
||||
.flatMap(_.crypto)
|
||||
.getOrElse(throw new IllegalArgumentException(s"instance $name is not running."))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait FeatureFlagFilter extends NamedLogging {
|
||||
|
||||
protected def consoleEnvironment: ConsoleEnvironment
|
||||
|
||||
protected def cantonConfig: CantonConfig = consoleEnvironment.environment.config
|
||||
|
||||
private def checkEnabled[T](flag: Boolean, config: String, command: => T): T =
|
||||
if (flag) {
|
||||
command
|
||||
} else {
|
||||
noTracingLogger.error(
|
||||
s"The command is currently disabled. You need to enable it explicitly by setting `canton.features.${config} = yes` in your Canton configuration file (`.conf`)"
|
||||
)
|
||||
throw new CommandFailure()
|
||||
}
|
||||
|
||||
protected def check[T](flag: FeatureFlag)(command: => T): T =
|
||||
checkEnabled(consoleEnvironment.featureSet.contains(flag), flag.configName, command)
|
||||
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import ammonite.runtime.Storage
|
||||
import cats.syntax.either.*
|
||||
import com.digitalasset.canton.logging.TracedLogger
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
|
||||
import java.io.{File, RandomAccessFile}
|
||||
import java.nio.channels.OverlappingFileLockException
|
||||
import scala.concurrent.blocking
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
trait AmmoniteCacheLock {
|
||||
def release(): Unit
|
||||
def storage: Storage
|
||||
def lockFile: Option[java.io.File]
|
||||
}
|
||||
|
||||
object AmmoniteCacheLock {
|
||||
|
||||
// Don't change this to lazy val, as the underlying InMemory storage is not thread safe.
|
||||
def InMemory: AmmoniteCacheLock = new AmmoniteCacheLock {
|
||||
override def release(): Unit = ()
|
||||
override val storage: Storage = Storage.InMemory()
|
||||
override def toString: String = "in-memory cache"
|
||||
override def lockFile: Option[File] = None
|
||||
}
|
||||
|
||||
def create(logger: TracedLogger, path: os.Path, isRepl: Boolean): AmmoniteCacheLock = {
|
||||
import TraceContext.Implicits.Empty.*
|
||||
def go(index: Int): Either[String, AmmoniteCacheLock] = {
|
||||
val newPath = path / s"$index"
|
||||
for {
|
||||
_ <- Either.cond(index < 255, (), s"Cache dir attempt reached $index, giving up")
|
||||
_ <- createDirsIfNecessary(newPath)
|
||||
_ <- ensureDirIsWritable(newPath)
|
||||
lockO <- acquireLock(logger, newPath, isRepl).leftMap { err =>
|
||||
logger.debug("Observed lock exception", err)
|
||||
err.getMessage
|
||||
}
|
||||
lock <- lockO match {
|
||||
case Some(value) => Right(value)
|
||||
case None => go(index + 1)
|
||||
}
|
||||
} yield lock
|
||||
}
|
||||
try {
|
||||
// check that cache directory is writable
|
||||
val attempt = for {
|
||||
_ <- createDirsIfNecessary(path)
|
||||
_ <- ensureDirIsWritable(path)
|
||||
lock <- go(0)
|
||||
} yield lock
|
||||
attempt match {
|
||||
case Right(lock) => lock
|
||||
case Left(err) =>
|
||||
logger.warn(
|
||||
s"Failed to acquire ammonite cache directory due to ${err}. Will use in-memory instead."
|
||||
)
|
||||
InMemory
|
||||
}
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
logger.warn("Failed to acquire ammonite cache directory. Will use in-memory instead.", e)
|
||||
InMemory
|
||||
}
|
||||
}
|
||||
|
||||
private def createDirsIfNecessary(path: os.Path): Either[String, Unit] =
|
||||
if (path.toIO.exists())
|
||||
Either.cond(path.toIO.isDirectory, (), s"File ${path} exists but is not a directory")
|
||||
else
|
||||
Either.cond(
|
||||
// create or test again (mkdirs fails if the directory exists in the meantime, which can happen
|
||||
// if several tests try to create the directory at the same time
|
||||
path.toIO.mkdirs() || path.toIO.exists(),
|
||||
(),
|
||||
s"Failed to create ammonite cache directory ${path}. Is the path writable?",
|
||||
)
|
||||
|
||||
private def ensureDirIsWritable(path: os.Path): Either[String, Unit] = {
|
||||
Either.cond(path.toIO.canWrite, (), s"Directory $path is not writable")
|
||||
}
|
||||
|
||||
private def acquireLock(logger: TracedLogger, path: os.Path, isRepl: Boolean)(implicit
|
||||
traceContext: TraceContext
|
||||
): Either[Throwable, Option[AmmoniteCacheLock]] = blocking(synchronized {
|
||||
try {
|
||||
val myLockFile = path / "lock"
|
||||
if (myLockFile.toIO.exists()) {
|
||||
Right(None)
|
||||
} else {
|
||||
logger.debug(s"Attempting to obtain lock ${myLockFile}")
|
||||
val out = new RandomAccessFile(myLockFile.toIO, "rw")
|
||||
Option(out.getChannel.tryLock()) match {
|
||||
case None =>
|
||||
logger.debug(s"Failed to acquire lock for ${myLockFile}")
|
||||
out.close()
|
||||
Right(None)
|
||||
case Some(lock) =>
|
||||
myLockFile.toIO.deleteOnExit()
|
||||
Right(Some(new AmmoniteCacheLock {
|
||||
override def release(): Unit = {
|
||||
try {
|
||||
logger.debug(s"Releasing lock $myLockFile...")
|
||||
lock.release()
|
||||
out.close()
|
||||
if (!myLockFile.toIO.delete()) {
|
||||
logger.warn(s"Failed to delete lock file ${myLockFile}")
|
||||
}
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
logger.error(s"Releasing ammonite cache lock $lockFile failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
override val storage: Storage = new Storage.Folder(path, isRepl = isRepl)
|
||||
|
||||
override def toString: String = s"file cache at $path"
|
||||
|
||||
override def lockFile: Option[File] = Some(myLockFile.toIO)
|
||||
}))
|
||||
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
case e: OverlappingFileLockException => Right(None)
|
||||
case NonFatal(e) => Left(e)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import ammonite.Main
|
||||
import ammonite.main.Defaults
|
||||
import ammonite.util.Colors
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.logging.TracedLogger
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
|
||||
import java.io.{File, IOException}
|
||||
|
||||
/** Configure behaviour of ammonite
|
||||
*
|
||||
* @param cacheDir cache dir, defaults to ~/.ammonite. If none is given, in-memory is used.
|
||||
* If you specify a cache dir, the Canton repl will startup faster.
|
||||
* In our tests, we have very rarely observed unexpected compile errors when the cache was enabled;
|
||||
* if you want to avoid that, set the cache dir to None (i.e. `cache-dir = null` in the config file).
|
||||
* @param workingDir working directory. if none is given, we'll use the working directory of the Canton process
|
||||
* @param colors if true (default), we'll use color output
|
||||
* @param verbose if true (not default), we'll emit verbose ammonite output
|
||||
* @param defaultLimit default limit parameter for commands that can potentially return many results
|
||||
*/
|
||||
final case class AmmoniteConsoleConfig(
|
||||
cacheDir: Option[java.io.File] = AmmoniteConsoleConfig.defaultCacheDir,
|
||||
workingDir: Option[java.io.File] = None,
|
||||
colors: Boolean = true,
|
||||
verbose: Boolean = false,
|
||||
defaultLimit: PositiveInt = PositiveInt.tryCreate(1000),
|
||||
)
|
||||
|
||||
object AmmoniteConsoleConfig extends LazyLogging {
|
||||
|
||||
private def defaultCacheDir: Option[java.io.File] = {
|
||||
val f = new File(System.getProperty("user.home"))
|
||||
if (f.exists() && f.isDirectory)
|
||||
Some(Defaults.ammoniteHome.toIO)
|
||||
else {
|
||||
logger.warn(
|
||||
s"""Can not determine user home directory using the java system property `user.home`
|
||||
| (is set to ${System.getProperty("user.home")}). Please set it
|
||||
|on jvm startup using -Duser.home=...""".stripMargin
|
||||
)
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
private def ensureTmpFilesCanBeCreated(): Unit = {
|
||||
try {
|
||||
val f = File.createTempFile("dummy", "test")
|
||||
val _ = f.delete()
|
||||
} catch {
|
||||
case e: IOException =>
|
||||
logger.error(
|
||||
"Unable to create temporary files (javas `File.createTempFile` throws an exception). Please make sure that the jvm can create files. The process will likely start to fail now.",
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private[console] def create(
|
||||
config: AmmoniteConsoleConfig,
|
||||
predefCode: String,
|
||||
welcomeBanner: Option[String],
|
||||
isRepl: Boolean,
|
||||
logger: TracedLogger,
|
||||
): (AmmoniteCacheLock, Main) = {
|
||||
val cacheLock: AmmoniteCacheLock = config.cacheDir match {
|
||||
case Some(file) => AmmoniteCacheLock.create(logger, os.Path(file), isRepl = isRepl)
|
||||
case None => AmmoniteCacheLock.InMemory
|
||||
}
|
||||
// ensure that we can create tmp files
|
||||
ensureTmpFilesCanBeCreated()
|
||||
val main = Main(
|
||||
predefCode = predefCode,
|
||||
storageBackend = cacheLock.storage,
|
||||
wd = config.workingDir.fold(os.pwd)(x => os.Path(x.getAbsolutePath)),
|
||||
welcomeBanner = welcomeBanner,
|
||||
verboseOutput =
|
||||
config.verbose, // disable things like "Compiling [x]..." messages from ammonite
|
||||
// ammonite when run as a binary will log the number of commands that are executed in a session for the maintainer to see usage
|
||||
// I don't think this happens when used in an embedded fashion like we're doing, but let's disable just to be sure ( ˇ෴ˇ )
|
||||
remoteLogging = false,
|
||||
colors = if (config.colors) Colors.Default else Colors.BlackWhite,
|
||||
)
|
||||
(cacheLock, main)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
/** Thrown when the bootstrap script fails to execute */
|
||||
class BootstrapScriptException(cause: Throwable)
|
||||
extends RuntimeException(s"Bootstrap script failed: ${cause.getMessage}", cause)
|
@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import better.files.File
|
||||
import cats.data.NonEmptyList
|
||||
import cats.syntax.parallel.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.codahale.metrics
|
||||
import com.digitalasset.canton.admin.api.client.data.{CantonStatus, CommunityCantonStatus}
|
||||
import com.digitalasset.canton.config.RequireTypes.Port
|
||||
import com.digitalasset.canton.config.{NonNegativeDuration, Password}
|
||||
import com.digitalasset.canton.health.admin.data.NodeStatus
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.topology.DomainId
|
||||
import com.digitalasset.canton.tracing.NoTracing
|
||||
import com.digitalasset.canton.util.FutureInstances.*
|
||||
import io.circe.{Encoder, Json, KeyEncoder, jawn}
|
||||
import io.opentelemetry.exporter.internal.otlp.metrics.ResourceMetricsMarshaler
|
||||
import io.opentelemetry.sdk.metrics.data.MetricData
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.time.Instant
|
||||
import scala.concurrent.duration.TimeUnit
|
||||
import scala.concurrent.{Await, ExecutionContext, Future, TimeoutException}
|
||||
import scala.jdk.CollectionConverters.SeqHasAsJava
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
object CantonHealthAdministrationEncoders {
|
||||
implicit val timeUnitEncoder: Encoder[TimeUnit] = Encoder.encodeString.contramap(_.toString)
|
||||
|
||||
implicit val snapshotEncoder: Encoder[metrics.Snapshot] =
|
||||
Encoder.forProduct4("mean", "std-dev", "p95", "median") { snapshot =>
|
||||
def toMs(nanos: Double): Double = nanos / 1e6
|
||||
(
|
||||
toMs(snapshot.getMean),
|
||||
toMs(snapshot.getStdDev),
|
||||
toMs(snapshot.get95thPercentile()),
|
||||
toMs(snapshot.getMedian),
|
||||
)
|
||||
}
|
||||
|
||||
implicit val counterEncoder: Encoder[metrics.Counter] = Encoder.forProduct1("count") { counter =>
|
||||
counter.getCount
|
||||
}
|
||||
implicit val gaugeEncoder: Encoder[metrics.Gauge[_]] = Encoder.forProduct1("gauge") { gauge =>
|
||||
gauge.getValue.toString
|
||||
}
|
||||
implicit val histoEncoder: Encoder[metrics.Histogram] =
|
||||
Encoder.forProduct1("hist")(_.getSnapshot)
|
||||
|
||||
implicit val meterEncoder: Encoder[metrics.Meter] =
|
||||
Encoder.forProduct3("count", "one-min-rate", "five-min-rate") { meter =>
|
||||
(meter.getCount, meter.getFiveMinuteRate, meter.getOneMinuteRate)
|
||||
}
|
||||
|
||||
implicit val timerEncoder: Encoder[metrics.Timer] =
|
||||
Encoder.forProduct4("count", "one-min-rate", "five-min-rate", "hist") { timer =>
|
||||
(timer.getCount, timer.getFiveMinuteRate, timer.getOneMinuteRate, timer.getSnapshot)
|
||||
}
|
||||
|
||||
/** Wraps the standardized log writer from OpenTelemetry, that outputs the metrics as JSON
|
||||
* Source: https://github.com/open-telemetry/opentelemetry-java/blob/main/exporters/logging-otlp/src/main/java/io/opentelemetry/exporter/logging/otlp/OtlpJsonLoggingMetricExporter.java
|
||||
* The encoder is not the most efficient as we first use the OpenTelemetry JSON serializer to write as a String,
|
||||
* and then use the Circe Jawn decoder to transform the string into a circe.Json object.
|
||||
* This is fine as the encoder is used only for on demand health dumps.
|
||||
*/
|
||||
implicit val openTelemetryMetricDataEncoder: Encoder[Seq[MetricData]] =
|
||||
Encoder.encodeSeq[Json].contramap[Seq[MetricData]] { metrics =>
|
||||
val resourceMetrics = ResourceMetricsMarshaler.create(metrics.asJava)
|
||||
resourceMetrics.toSeq.map { resource =>
|
||||
val byteArrayOutputStream = new ByteArrayOutputStream()
|
||||
resource.writeJsonTo(byteArrayOutputStream)
|
||||
jawn
|
||||
.decode[Json](byteArrayOutputStream.toString)
|
||||
.fold(
|
||||
error => Json.fromString(s"Failed to decode metrics: $error"),
|
||||
identity,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val traceElemEncoder: Encoder[StackTraceElement] =
|
||||
Encoder.encodeString.contramap(_.toString)
|
||||
implicit val threadKeyEncoder: KeyEncoder[Thread] = (thread: Thread) => thread.getName
|
||||
|
||||
implicit val domainIdEncoder: KeyEncoder[DomainId] = (ref: DomainId) => ref.toString
|
||||
|
||||
implicit val encodePort: Encoder[Port] = Encoder.encodeInt.contramap[Port](_.unwrap)
|
||||
|
||||
// We do not want to serialize the password to JSON, e.g., as part of a config dump.
|
||||
implicit val encoder: Encoder[Password] = Encoder.encodeString.contramap(_ => "****")
|
||||
}
|
||||
|
||||
object CantonHealthAdministration {
|
||||
def defaultHealthDumpName: File = {
|
||||
// Replace ':' in the timestamp as they are forbidden on windows
|
||||
val name = s"canton-dump-${Instant.now().toString.replace(':', '-')}.zip"
|
||||
File(name)
|
||||
}
|
||||
}
|
||||
|
||||
trait CantonHealthAdministration[Status <: CantonStatus]
|
||||
extends Helpful
|
||||
with NamedLogging
|
||||
with NoTracing {
|
||||
protected val consoleEnv: ConsoleEnvironment
|
||||
implicit private val ec: ExecutionContext = consoleEnv.environment.executionContext
|
||||
override val loggerFactory: NamedLoggerFactory = consoleEnv.environment.loggerFactory
|
||||
|
||||
protected def statusMap[A <: InstanceReferenceCommon](
|
||||
nodes: NodeReferences[A, _, _]
|
||||
): Map[String, () => NodeStatus[A#Status]] = {
|
||||
nodes.all.map { node => node.name -> (() => node.health.status) }.toMap
|
||||
}
|
||||
|
||||
def status(): Status
|
||||
|
||||
@Help.Summary("Generate and write a health dump of Canton's state for a bug report")
|
||||
@Help.Description(
|
||||
"Gathers information about the current Canton process and/or remote nodes if using the console" +
|
||||
" with a remote config. The outputFile argument can be used to write the health dump to a specific path." +
|
||||
" The timeout argument can be increased when retrieving large health dumps from remote nodes." +
|
||||
" The chunkSize argument controls the size of the byte chunks streamed back from remote nodes. This can be used" +
|
||||
" if encountering errors due to gRPC max inbound message size being too low."
|
||||
)
|
||||
def dump(
|
||||
outputFile: File = CantonHealthAdministration.defaultHealthDumpName,
|
||||
timeout: NonNegativeDuration = consoleEnv.commandTimeouts.ledgerCommand,
|
||||
chunkSize: Option[Int] = None,
|
||||
): String = {
|
||||
val remoteDumps = consoleEnv.nodes.remote.toList.parTraverse { n =>
|
||||
Future {
|
||||
n.health.dump(
|
||||
File.newTemporaryFile(s"remote-${n.name}-"),
|
||||
timeout,
|
||||
chunkSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get a local dump by going through the local nodes and returning the first one that succeeds
|
||||
def getLocalDump(nodes: NonEmptyList[InstanceReferenceCommon]): Future[String] = {
|
||||
Future {
|
||||
nodes.head.health.dump(
|
||||
File.newTemporaryFile(s"local-"),
|
||||
timeout,
|
||||
chunkSize,
|
||||
)
|
||||
}.recoverWith { case NonFatal(e) =>
|
||||
NonEmptyList.fromList(nodes.tail) match {
|
||||
case Some(tail) =>
|
||||
logger.info(
|
||||
s"Could not get health dump from ${nodes.head.name}, trying the next local node",
|
||||
e,
|
||||
)
|
||||
getLocalDump(tail)
|
||||
case None => Future.failed(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val localDump = NonEmptyList
|
||||
// The sorting is not necessary but makes testing easier
|
||||
.fromList(consoleEnv.nodes.local.toList.sortBy(_.name))
|
||||
.traverse(getLocalDump)
|
||||
.map(_.toList)
|
||||
|
||||
consoleEnv.run {
|
||||
val zippedHealthDump = List(remoteDumps, localDump).flatSequence.map { allDumps =>
|
||||
outputFile.zipIn(allDumps.map(File(_)).iterator).pathAsString
|
||||
}
|
||||
Try(Await.result(zippedHealthDump, timeout.duration)) match {
|
||||
case Success(result) => CommandSuccessful(result)
|
||||
case Failure(e: TimeoutException) =>
|
||||
CommandErrors.ConsoleTimeout.Error(timeout.asJavaApproximation)
|
||||
case Failure(exception) => CommandErrors.CommandInternalError.ErrorWithException(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CommunityCantonHealthAdministration(override val consoleEnv: ConsoleEnvironment)
|
||||
extends CantonHealthAdministration[CommunityCantonStatus] {
|
||||
|
||||
@Help.Summary("Aggregate status info of all participants and domains")
|
||||
def status(): CommunityCantonStatus = {
|
||||
CommunityCantonStatus.getStatus(
|
||||
statusMap[DomainReference](consoleEnv.domains),
|
||||
statusMap[ParticipantReference](consoleEnv.participants),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.data.CommunityCantonStatus
|
||||
import com.digitalasset.canton.environment.CommunityEnvironment
|
||||
import com.digitalasset.canton.health.admin.data.{DomainStatus, ParticipantStatus}
|
||||
import io.circe.Encoder
|
||||
import io.circe.generic.semiauto.deriveEncoder
|
||||
|
||||
import scala.annotation.nowarn
|
||||
|
||||
@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072
|
||||
class CommunityHealthDumpGenerator(
|
||||
override val environment: CommunityEnvironment,
|
||||
override val grpcAdminCommandRunner: GrpcAdminCommandRunner,
|
||||
) extends HealthDumpGenerator[CommunityCantonStatus] {
|
||||
override protected implicit val statusEncoder: Encoder[CommunityCantonStatus] = {
|
||||
import io.circe.generic.auto.*
|
||||
import CantonHealthAdministrationEncoders.*
|
||||
deriveEncoder[CommunityCantonStatus]
|
||||
}
|
||||
|
||||
override def status(): CommunityCantonStatus = {
|
||||
CommunityCantonStatus.getStatus(
|
||||
statusMap(environment.config.domainsByString, DomainStatus.fromProtoV0),
|
||||
statusMap(environment.config.participantsByString, ParticipantStatus.fromProtoV0),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import cats.Monad
|
||||
import cats.syntax.alternative.*
|
||||
import com.daml.error.{ErrorCategory, ErrorCode}
|
||||
import com.digitalasset.canton.console.CommandErrors.{CommandError, GenericCommandError}
|
||||
import com.digitalasset.canton.error.CantonErrorGroups.CommandErrorGroup
|
||||
import com.digitalasset.canton.error.*
|
||||
import com.digitalasset.canton.util.ErrorUtil
|
||||
import org.slf4j.event.Level
|
||||
|
||||
import java.time.Duration
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
/** Response from a console command.
|
||||
*/
|
||||
sealed trait ConsoleCommandResult[+A] {
|
||||
def toEither: Either[String, A]
|
||||
|
||||
def flatMap[B](f: A => ConsoleCommandResult[B]): ConsoleCommandResult[B] = this match {
|
||||
case CommandSuccessful(a) => f(a)
|
||||
case err: CommandError => err
|
||||
}
|
||||
|
||||
def map[B](f: A => B): ConsoleCommandResult[B] = this match {
|
||||
case CommandSuccessful(a) => CommandSuccessful(f(a))
|
||||
case err: CommandError => err
|
||||
}
|
||||
}
|
||||
|
||||
object ConsoleCommandResult {
|
||||
|
||||
implicit val consoleCommandResultMonad: Monad[ConsoleCommandResult] =
|
||||
new Monad[ConsoleCommandResult] {
|
||||
override def flatMap[A, B](fa: ConsoleCommandResult[A])(
|
||||
f: A => ConsoleCommandResult[B]
|
||||
): ConsoleCommandResult[B] = fa.flatMap(f)
|
||||
|
||||
override def tailRecM[A, B](
|
||||
a: A
|
||||
)(f: A => ConsoleCommandResult[Either[A, B]]): ConsoleCommandResult[B] = {
|
||||
def go(ccr: ConsoleCommandResult[Either[A, B]]): ConsoleCommandResult[B] = ccr match {
|
||||
case CommandSuccessful(Left(a)) => go(f(a))
|
||||
case CommandSuccessful(Right(b)) => CommandSuccessful(b)
|
||||
case err: CommandError => err
|
||||
}
|
||||
go(CommandSuccessful(Left(a)))
|
||||
}
|
||||
|
||||
override def pure[A](x: A): ConsoleCommandResult[A] = CommandSuccessful(x)
|
||||
}
|
||||
|
||||
def fromEither[A](either: Either[String, A]): ConsoleCommandResult[A] =
|
||||
either match {
|
||||
case Left(err) => GenericCommandError(err)
|
||||
case Right(value) => CommandSuccessful(value)
|
||||
}
|
||||
|
||||
private[console] def runAll[Instance <: InstanceReferenceCommon, Result](
|
||||
instances: Seq[Instance]
|
||||
)(
|
||||
action: Instance => ConsoleCommandResult[Result]
|
||||
)(implicit consoleEnvironment: ConsoleEnvironment): Map[Instance, Result] =
|
||||
consoleEnvironment.run {
|
||||
forAll(instances)(action)
|
||||
}
|
||||
|
||||
/** Call a console command on all instances.
|
||||
* Will run all in sequence and will merge all failures.
|
||||
* If nothing fails, the final CommandSuccessful result will be returned.
|
||||
* @param action Action to perform on instances
|
||||
* @return Successful if the action was successful for all instances, otherwise all the errors encountered merged into one.
|
||||
*/
|
||||
private[console] def forAll[Instance <: InstanceReferenceCommon, Result](
|
||||
instances: Seq[Instance]
|
||||
)(
|
||||
action: Instance => ConsoleCommandResult[Result]
|
||||
): ConsoleCommandResult[Map[Instance, Result]] = {
|
||||
val (errors, results) = instances
|
||||
.map(instance => instance -> Try(action(instance)))
|
||||
.map {
|
||||
case (instance, Success(CommandSuccessful(value))) => Right(instance -> value)
|
||||
case (instance, Success(err: CommandError)) =>
|
||||
Left(
|
||||
s"(failure on ${instance.name}): ${err.cause}"
|
||||
)
|
||||
case (instance, Failure(t)) =>
|
||||
Left(s"(exception on ${instance.name}: ${ErrorUtil.messageWithStacktrace(t)}")
|
||||
}
|
||||
.toList
|
||||
.separate
|
||||
if (errors.isEmpty) {
|
||||
CommandSuccessful(results.toMap)
|
||||
} else {
|
||||
GenericCommandError(
|
||||
s"Command failed on ${errors.length} out of ${instances.length} instances: ${errors.mkString(", ")}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Successful command result
|
||||
* @param value The value returned from the command
|
||||
*/
|
||||
final case class CommandSuccessful[+A](value: A) extends ConsoleCommandResult[A] {
|
||||
override lazy val toEither: Either[String, A] = Right(value)
|
||||
}
|
||||
|
||||
object CommandSuccessful {
|
||||
def apply(): CommandSuccessful[Unit] = CommandSuccessful(())
|
||||
}
|
||||
|
||||
// Each each in object CommandErrors, will have an error code that begins with `CA12` ('CA1' due to inheritance from CommunityAppError, '2' due to the argument)
|
||||
object CommandErrors extends CommandErrorGroup {
|
||||
|
||||
sealed trait CommandError extends ConsoleCommandResult[Nothing] {
|
||||
override lazy val toEither: Either[String, Nothing] = Left(cause)
|
||||
def cause: String
|
||||
}
|
||||
|
||||
sealed abstract class CantonCommandError(
|
||||
override val cause: String,
|
||||
override val throwableO: Option[Throwable] = None,
|
||||
)(implicit override val code: ErrorCode)
|
||||
extends BaseCantonError
|
||||
with CommandError
|
||||
|
||||
sealed abstract class CommandErrorCode(id: String, category: ErrorCategory)
|
||||
extends ErrorCode(id, category) {
|
||||
override def errorConveyanceDocString: Option[String] = Some(
|
||||
"These errors are shown as errors on the console."
|
||||
)
|
||||
}
|
||||
|
||||
object CommandInternalError
|
||||
extends CommandErrorCode(
|
||||
"CONSOLE_COMMAND_INTERNAL_ERROR",
|
||||
ErrorCategory.SystemInternalAssumptionViolated,
|
||||
) {
|
||||
final case class ErrorWithException(throwable: Throwable)
|
||||
extends CantonCommandError(
|
||||
"An internal error has occurred while running a console command.",
|
||||
Some(throwable),
|
||||
)
|
||||
final case class NullError()
|
||||
extends CantonCommandError("Console command has returned 'null' as result.")
|
||||
}
|
||||
|
||||
// The majority of the use cases of this error are for generic Either[..., ...] => ConsoleCommandResult[...] conversions
|
||||
// Thus, it doesn't have an error code because the underlying error that is wrapped should provide the error code
|
||||
// TODO(i6183) - replace uses of this wrapper with a CantonParentError wrapper except when parsing gRPC errors
|
||||
final case class GenericCommandError(cause: String)
|
||||
extends ConsoleCommandResult[Nothing]
|
||||
with CommandError
|
||||
|
||||
object ConsoleTimeout
|
||||
extends CommandErrorCode(
|
||||
"CONSOLE_COMMAND_TIMED_OUT",
|
||||
ErrorCategory.SystemInternalAssumptionViolated,
|
||||
) {
|
||||
final case class Error(timeout: Duration)
|
||||
extends CantonCommandError(s"Condition never became true after ${timeout}")
|
||||
}
|
||||
|
||||
object NodeNotStarted
|
||||
extends CommandErrorCode(
|
||||
"NODE_NOT_STARTED",
|
||||
ErrorCategory.InvalidGivenCurrentSystemStateOther,
|
||||
) {
|
||||
override def logLevel: Level = Level.ERROR
|
||||
final case class ErrorCanton(instance: LocalInstanceReferenceCommon)
|
||||
extends CantonCommandError(s"Instance $instance has not been started. ")
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,646 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import ammonite.util.Bind
|
||||
import cats.syntax.either.*
|
||||
import com.digitalasset.canton.admin.api.client.data.CantonStatus
|
||||
import com.digitalasset.canton.config.CantonRequireTypes.InstanceName
|
||||
import com.digitalasset.canton.config.RequireTypes.{NonNegativeInt, PositiveDouble, PositiveInt}
|
||||
import com.digitalasset.canton.config.{
|
||||
ConsoleCommandTimeout,
|
||||
NonNegativeDuration,
|
||||
NonNegativeFiniteDuration,
|
||||
PositiveDurationSeconds,
|
||||
ProcessingTimeout,
|
||||
}
|
||||
import com.digitalasset.canton.console.CommandErrors.{
|
||||
CantonCommandError,
|
||||
CommandInternalError,
|
||||
GenericCommandError,
|
||||
}
|
||||
import com.digitalasset.canton.console.Help.{Description, Summary, Topic}
|
||||
import com.digitalasset.canton.crypto.Fingerprint
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.environment.Environment
|
||||
import com.digitalasset.canton.lifecycle.{FlagCloseable, Lifecycle}
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.protocol.SerializableContract
|
||||
import com.digitalasset.canton.sequencing.{
|
||||
GrpcSequencerConnection,
|
||||
SequencerConnection,
|
||||
SequencerConnections,
|
||||
}
|
||||
import com.digitalasset.canton.time.SimClock
|
||||
import com.digitalasset.canton.topology.{Identifier, ParticipantId, PartyId}
|
||||
import com.digitalasset.canton.tracing.{NoTracing, TraceContext, TracerProvider}
|
||||
import com.digitalasset.canton.util.EitherUtil
|
||||
import com.digitalasset.canton.{DomainAlias, LfPartyId}
|
||||
import com.typesafe.scalalogging.Logger
|
||||
import io.opentelemetry.api.trace.Tracer
|
||||
|
||||
import java.time.{Duration as JDuration, Instant}
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import scala.concurrent.duration.Duration as SDuration
|
||||
import scala.reflect.runtime.universe as ru
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
final case class NodeReferences[A, R <: A, L <: A](local: Seq[L], remote: Seq[R]) {
|
||||
val all: Seq[A] = local ++ remote
|
||||
}
|
||||
|
||||
/** The environment in which console commands are evaluated.
|
||||
*/
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any")) // required for `Binding[_]` usage
|
||||
trait ConsoleEnvironment extends NamedLogging with FlagCloseable with NoTracing {
|
||||
type Env <: Environment
|
||||
type DomainLocalRef <: LocalDomainReference
|
||||
type DomainRemoteRef <: RemoteDomainReference
|
||||
type Status <: CantonStatus
|
||||
|
||||
def consoleLogger: Logger = super.noTracingLogger
|
||||
|
||||
def health: CantonHealthAdministration[Status]
|
||||
|
||||
/** the underlying Canton runtime environment */
|
||||
val environment: Env
|
||||
|
||||
/** determines the control exception thrown on errors */
|
||||
val errorHandler: ConsoleErrorHandler = ThrowErrorHandler
|
||||
|
||||
/** the console for user facing output */
|
||||
val consoleOutput: ConsoleOutput
|
||||
|
||||
/** The predef code itself which is executed before any script or repl command */
|
||||
private[console] def predefCode(interactive: Boolean, noTty: Boolean = false): String =
|
||||
consoleEnvironmentBindings.predefCode(interactive, noTty)
|
||||
protected def consoleEnvironmentBindings: ConsoleEnvironmentBinding
|
||||
|
||||
private val tracerProvider =
|
||||
TracerProvider.Factory(environment.configuredOpenTelemetry, "console")
|
||||
private[console] val tracer: Tracer = tracerProvider.tracer
|
||||
|
||||
/** Definition of the startup order of local instances.
|
||||
* Nodes support starting up in any order however to avoid delays/warnings we opt to start in the most desirable order
|
||||
* for simple execution. (e.g. domains started before participants).
|
||||
* Implementations should just return a int for the instance (typically just a static value based on type),
|
||||
* and then the console will start these instances for lower to higher values.
|
||||
*/
|
||||
protected def startupOrderPrecedence(instance: LocalInstanceReferenceCommon): Int
|
||||
|
||||
/** The order that local nodes would ideally be started in. */
|
||||
final val startupOrdering: Ordering[LocalInstanceReferenceCommon] =
|
||||
(x: LocalInstanceReferenceCommon, y: LocalInstanceReferenceCommon) =>
|
||||
startupOrderPrecedence(x) compare startupOrderPrecedence(y)
|
||||
|
||||
/** allows for injecting a custom admin command runner during tests */
|
||||
protected def createAdminCommandRunner: ConsoleEnvironment => ConsoleGrpcAdminCommandRunner
|
||||
|
||||
protected override val loggerFactory: NamedLoggerFactory = environment.loggerFactory
|
||||
|
||||
private val commandTimeoutReference: AtomicReference[ConsoleCommandTimeout] =
|
||||
new AtomicReference[ConsoleCommandTimeout](environment.config.parameters.timeouts.console)
|
||||
|
||||
private val featureSetReference: AtomicReference[HelperItems] =
|
||||
new AtomicReference[HelperItems](HelperItems(environment.config.features.featureFlags))
|
||||
|
||||
/** Generate implementation specific help items for local domains */
|
||||
protected def localDomainHelpItems(
|
||||
scope: Set[FeatureFlag],
|
||||
localDomain: DomainLocalRef,
|
||||
): Seq[Help.Item]
|
||||
|
||||
/** Generate implementation specific help items for remote domains */
|
||||
protected def remoteDomainHelpItems(
|
||||
scope: Set[FeatureFlag],
|
||||
remoteDomain: DomainRemoteRef,
|
||||
): Seq[Help.Item]
|
||||
|
||||
private case class HelperItems(scope: Set[FeatureFlag]) {
|
||||
lazy val participantHelperItems = {
|
||||
// due to the use of reflection to grab the help-items, i need to write the following, repetitive stuff explicitly
|
||||
val subItems =
|
||||
if (participants.local.nonEmpty)
|
||||
participants.local.headOption.toList.flatMap(p =>
|
||||
Help.getItems(p, baseTopic = Seq("$participant"), scope = scope)
|
||||
)
|
||||
else if (participants.remote.nonEmpty)
|
||||
participants.remote.headOption.toList.flatMap(p =>
|
||||
Help.getItems(p, baseTopic = Seq("$participant"), scope = scope)
|
||||
)
|
||||
else Seq()
|
||||
Help.Item("$participant", None, Summary(""), Description(""), Topic(Seq()), subItems)
|
||||
}
|
||||
|
||||
lazy val domainHelperItems = {
|
||||
val subItems =
|
||||
if (domains.local.nonEmpty)
|
||||
domains.local.headOption.toList.flatMap(localDomainHelpItems(scope, _))
|
||||
else if (domains.remote.nonEmpty)
|
||||
domains.remote.headOption.toList.flatMap(remoteDomainHelpItems(scope, _))
|
||||
else Seq()
|
||||
Help.Item("$domain", None, Summary(""), Description(""), Topic(Seq()), subItems)
|
||||
}
|
||||
|
||||
lazy val filteredHelpItems = {
|
||||
helpItems.filter(x => scope.contains(x.summary.flag))
|
||||
}
|
||||
|
||||
lazy val all = filteredHelpItems :+ participantHelperItems :+ domainHelperItems
|
||||
|
||||
}
|
||||
|
||||
protected def timeouts: ProcessingTimeout = environment.config.parameters.timeouts.processing
|
||||
|
||||
/** @return maximum runtime of a console command
|
||||
*/
|
||||
def commandTimeouts: ConsoleCommandTimeout = commandTimeoutReference.get()
|
||||
|
||||
def setCommandTimeout(newTimeout: NonNegativeDuration): Unit = {
|
||||
require(newTimeout.duration > SDuration.Zero, "The command timeout must be positive!")
|
||||
commandTimeoutReference.updateAndGet(cur => cur.copy(bounded = newTimeout)).discard
|
||||
}
|
||||
|
||||
def setLedgerCommandTimeout(newTimeout: NonNegativeDuration): Unit = {
|
||||
require(newTimeout.duration > SDuration.Zero, "The ledger command timeout must be positive!")
|
||||
commandTimeoutReference.updateAndGet(cur => cur.copy(ledgerCommand = newTimeout)).discard
|
||||
}
|
||||
|
||||
/** returns the currently enabled feature sets */
|
||||
def featureSet: Set[FeatureFlag] = featureSetReference.get().scope
|
||||
|
||||
def updateFeatureSet(flag: FeatureFlag, include: Boolean): Unit = {
|
||||
val _ = featureSetReference.updateAndGet { x =>
|
||||
val scope = if (include) x.scope + flag else x.scope - flag
|
||||
HelperItems(scope)
|
||||
}
|
||||
}
|
||||
|
||||
/** Holder for top level values including their name, their value, and a description to display when `help` is printed.
|
||||
*/
|
||||
protected case class TopLevelValue[T](
|
||||
nameUnsafe: String,
|
||||
summary: String,
|
||||
value: T,
|
||||
topic: Seq[String] = Seq(),
|
||||
)(implicit tag: ru.TypeTag[T]) {
|
||||
|
||||
// Surround with back-ticks to handle the case that name is a reserved keyword in scala.
|
||||
lazy val asBind: Either[InstanceName.InvalidInstanceName, Bind[T]] =
|
||||
InstanceName.create(nameUnsafe).map(name => Bind(s"`${name.unwrap}`", value))
|
||||
|
||||
lazy val asHelpItem: Help.Item =
|
||||
Help.Item(nameUnsafe, None, Help.Summary(summary), Help.Description(""), Help.Topic(topic))
|
||||
}
|
||||
|
||||
object TopLevelValue {
|
||||
|
||||
/** Provide all details but the value itself. A subsequent call can then specify the value from another location.
|
||||
* This oddness is to allow the ConsoleEnvironment implementations to specify the values of node instances they
|
||||
* use as scala's runtime reflection can't easily take advantage of the type members we have available here.
|
||||
*/
|
||||
case class Partial(name: String, summary: String, topics: Seq[String] = Seq.empty) {
|
||||
def apply[T](value: T)(implicit t: ru.TypeTag[T]): TopLevelValue[T] =
|
||||
TopLevelValue(name, summary, value, topics)
|
||||
}
|
||||
}
|
||||
|
||||
// lazy to prevent publication of this before this has been fully initialized
|
||||
lazy val grpcAdminCommandRunner: ConsoleGrpcAdminCommandRunner = createAdminCommandRunner(this)
|
||||
|
||||
def runE[E, A](result: => Either[E, A]): A = {
|
||||
run(ConsoleCommandResult.fromEither(result.leftMap(_.toString)))
|
||||
}
|
||||
|
||||
/** Run a console command.
|
||||
*/
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Null"))
|
||||
def run[A](result: => ConsoleCommandResult[A]): A = {
|
||||
val resultValue: ConsoleCommandResult[A] =
|
||||
try {
|
||||
result
|
||||
} catch {
|
||||
case err: Throwable =>
|
||||
CommandInternalError.ErrorWithException(err).logWithContext()
|
||||
err match {
|
||||
case NonFatal(_) =>
|
||||
// No need to rethrow err, as it has been logged and output
|
||||
errorHandler.handleInternalError()
|
||||
case _ =>
|
||||
// Rethrow err, as it is a bad practice to discard fatal errors.
|
||||
// As a result, the error may be printed several times,
|
||||
// but there is no guarantee that the log is still working.
|
||||
// So it is better to err on the safe side.
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
def invocationContext(): Map[String, String] =
|
||||
findInvocationSite() match {
|
||||
case Some((funcName, callSite)) => Map("function" -> funcName, "callsite" -> callSite)
|
||||
case None => Map()
|
||||
}
|
||||
|
||||
resultValue match {
|
||||
case null =>
|
||||
CommandInternalError.NullError().logWithContext(invocationContext())
|
||||
errorHandler.handleInternalError()
|
||||
case CommandSuccessful(value) =>
|
||||
value
|
||||
case err: CantonCommandError =>
|
||||
err.logWithContext(invocationContext())
|
||||
errorHandler.handleCommandFailure()
|
||||
case err: GenericCommandError =>
|
||||
val errMsg = findInvocationSite() match {
|
||||
case Some((funcName, site)) =>
|
||||
err.cause + s"\n Command ${funcName} invoked from ${site}"
|
||||
case None => err.cause
|
||||
}
|
||||
logger.error(errMsg)
|
||||
errorHandler.handleCommandFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private def findInvocationSite(): Option[(String, String)] = {
|
||||
val stack = Thread.currentThread().getStackTrace
|
||||
// assumption: first few stack elements are all in our set of known packages. our call-site is
|
||||
// the first entry outside of our package
|
||||
// also skip all scala packages because a collection's map operation is not an informative call site
|
||||
val myPackages =
|
||||
Seq("com.digitalasset.canton.console", "com.digitalasset.canton.environment", "scala.")
|
||||
|
||||
def isKnown(element: StackTraceElement): Boolean =
|
||||
myPackages.exists(element.getClassName.startsWith)
|
||||
|
||||
stack.sliding(2).collectFirst {
|
||||
case Array(callee, caller) if isKnown(callee) && !isKnown(caller) =>
|
||||
val drop = callee.getClassName.lastIndexOf(".") + 1
|
||||
val funcName = callee.getClassName.drop(drop) + "." + callee.getMethodName
|
||||
(funcName, s"${caller.getFileName}:${caller.getLineNumber}")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Print help for items in the top level scope.
|
||||
*/
|
||||
def help(): Unit = {
|
||||
consoleOutput.info(Help.format(featureSetReference.get().filteredHelpItems: _*))
|
||||
}
|
||||
|
||||
/** Print detailed help for a top-level item in the top level scope.
|
||||
*/
|
||||
def help(cmd: String): Unit =
|
||||
consoleOutput.info(Help.forMethod(featureSetReference.get().all, cmd))
|
||||
|
||||
def helpItems: Seq[Help.Item] =
|
||||
topLevelValues.map(_.asHelpItem) ++
|
||||
Help.fromObject(ConsoleMacros) ++
|
||||
Help.fromObject(this) :+
|
||||
(
|
||||
Help.Item(
|
||||
"help",
|
||||
None,
|
||||
Help.Summary(
|
||||
"Help with console commands; type help(\"<command>\") for detailed help for <command>"
|
||||
),
|
||||
Help.Description(""),
|
||||
Help.Topic(Help.defaultTopLevelTopic),
|
||||
),
|
||||
) :+
|
||||
(Help.Item(
|
||||
"exit",
|
||||
None,
|
||||
Help.Summary("Leave the console"),
|
||||
Help.Description(""),
|
||||
Help.Topic(Help.defaultTopLevelTopic),
|
||||
))
|
||||
|
||||
lazy val participants: NodeReferences[
|
||||
ParticipantReference,
|
||||
RemoteParticipantReference,
|
||||
LocalParticipantReference,
|
||||
] =
|
||||
NodeReferences(
|
||||
environment.config.participantsByString.keys.map(createParticipantReference).toSeq,
|
||||
environment.config.remoteParticipantsByString.keys
|
||||
.map(createRemoteParticipantReference)
|
||||
.toSeq,
|
||||
)
|
||||
|
||||
lazy val participantsX: NodeReferences[
|
||||
ParticipantReferenceX,
|
||||
RemoteParticipantReferenceX,
|
||||
LocalParticipantReferenceX,
|
||||
] =
|
||||
NodeReferences(
|
||||
environment.config.participantsByStringX.keys.map(createParticipantReferenceX).toSeq,
|
||||
environment.config.remoteParticipantsByStringX.keys
|
||||
.map(createRemoteParticipantReferenceX)
|
||||
.toSeq,
|
||||
)
|
||||
|
||||
lazy val domains: NodeReferences[DomainReference, DomainRemoteRef, DomainLocalRef] =
|
||||
NodeReferences(
|
||||
environment.config.domainsByString.keys.map(createDomainReference).toSeq,
|
||||
environment.config.remoteDomainsByString.keys.map(createRemoteDomainReference).toSeq,
|
||||
)
|
||||
|
||||
// the scala compiler / wartremover gets confused here if I use ++ directly
|
||||
def mergeLocalInstances(
|
||||
locals: Seq[LocalInstanceReferenceCommon]*
|
||||
): Seq[LocalInstanceReferenceCommon] =
|
||||
locals.flatten
|
||||
def mergeRemoteInstances(remotes: Seq[InstanceReferenceCommon]*): Seq[InstanceReferenceCommon] =
|
||||
remotes.flatten
|
||||
|
||||
lazy val nodes: NodeReferences[
|
||||
InstanceReferenceCommon,
|
||||
InstanceReferenceCommon,
|
||||
LocalInstanceReferenceCommon,
|
||||
] = {
|
||||
NodeReferences(
|
||||
mergeLocalInstances(participants.local, participantsX.local, domains.local),
|
||||
mergeRemoteInstances(participants.remote, participantsX.remote, domains.remote),
|
||||
)
|
||||
}
|
||||
|
||||
protected def helpText(typeName: String, name: String) =
|
||||
s"Manage $typeName '${name}'; type '${name} help' or '${name} help" + "(\"<methodName>\")' for more help"
|
||||
|
||||
protected val topicNodeReferences = "Node References"
|
||||
protected val topicGenericNodeReferences = "Generic Node References"
|
||||
protected val genericNodeReferencesDoc = " (.all, .local, .remote)"
|
||||
|
||||
protected def domainsTopLevelValue(
|
||||
h: TopLevelValue.Partial,
|
||||
domains: NodeReferences[DomainReference, DomainRemoteRef, DomainLocalRef],
|
||||
): TopLevelValue[NodeReferences[DomainReference, DomainRemoteRef, DomainLocalRef]]
|
||||
|
||||
/** Supply the local domain value used by the implementation */
|
||||
protected def localDomainTopLevelValue(
|
||||
h: TopLevelValue.Partial,
|
||||
d: DomainLocalRef,
|
||||
): TopLevelValue[DomainLocalRef]
|
||||
|
||||
/** Supply the remote domain value used by the implementation */
|
||||
protected def remoteDomainTopLevelValue(
|
||||
h: TopLevelValue.Partial,
|
||||
d: DomainRemoteRef,
|
||||
): TopLevelValue[DomainRemoteRef]
|
||||
|
||||
/** Assemble top level values with their identifier name, value binding, and help description.
|
||||
*/
|
||||
protected def topLevelValues: Seq[TopLevelValue[_]] = {
|
||||
val nodeTopic = Seq(topicNodeReferences)
|
||||
val localParticipantBinds: Seq[TopLevelValue[_]] =
|
||||
participants.local.map(p =>
|
||||
TopLevelValue(p.name, helpText("participant", p.name), p, nodeTopic)
|
||||
)
|
||||
val remoteParticipantBinds: Seq[TopLevelValue[_]] =
|
||||
participants.remote.map(p =>
|
||||
TopLevelValue(p.name, helpText("remote participant", p.name), p, nodeTopic)
|
||||
)
|
||||
val localParticipantXBinds: Seq[TopLevelValue[_]] =
|
||||
participantsX.local.map(p =>
|
||||
TopLevelValue(p.name, helpText("participant x", p.name), p, nodeTopic)
|
||||
)
|
||||
val remoteParticipantXBinds: Seq[TopLevelValue[_]] =
|
||||
participantsX.remote.map(p =>
|
||||
TopLevelValue(p.name, helpText("remote participant x", p.name), p, nodeTopic)
|
||||
)
|
||||
val localDomainBinds: Seq[TopLevelValue[_]] =
|
||||
domains.local.map(d =>
|
||||
localDomainTopLevelValue(
|
||||
TopLevelValue.Partial(d.name, helpText("local domain", d.name), nodeTopic),
|
||||
d,
|
||||
)
|
||||
)
|
||||
val remoteDomainBinds: Seq[TopLevelValue[_]] =
|
||||
domains.remote.map(d =>
|
||||
remoteDomainTopLevelValue(
|
||||
TopLevelValue.Partial(d.name, helpText("remote domain", d.name), nodeTopic),
|
||||
d,
|
||||
)
|
||||
)
|
||||
val clockBinds: Option[TopLevelValue[_]] =
|
||||
environment.simClock.map(cl =>
|
||||
TopLevelValue("clock", "Simulated time", new SimClockCommand(cl))
|
||||
)
|
||||
val referencesTopic = Seq(topicGenericNodeReferences)
|
||||
localParticipantBinds ++ remoteParticipantBinds ++
|
||||
localParticipantXBinds ++ remoteParticipantXBinds ++
|
||||
localDomainBinds ++ remoteDomainBinds ++ clockBinds.toList :+
|
||||
TopLevelValue(
|
||||
"participants",
|
||||
"All participant nodes" + genericNodeReferencesDoc,
|
||||
participants,
|
||||
referencesTopic,
|
||||
) :+
|
||||
TopLevelValue(
|
||||
"participantsX",
|
||||
"All participant x nodes" + genericNodeReferencesDoc,
|
||||
participantsX,
|
||||
referencesTopic,
|
||||
) :+
|
||||
domainsTopLevelValue(
|
||||
TopLevelValue
|
||||
.Partial("domains", "All domain nodes" + genericNodeReferencesDoc, referencesTopic),
|
||||
domains,
|
||||
) :+
|
||||
TopLevelValue("nodes", "All nodes" + genericNodeReferencesDoc, nodes, referencesTopic)
|
||||
}
|
||||
|
||||
/** Bindings for ammonite
|
||||
* Add a reference to this instance to resolve implicit references within the console
|
||||
*/
|
||||
lazy val bindings: Either[RuntimeException, IndexedSeq[Bind[_]]] = {
|
||||
import cats.syntax.traverse.*
|
||||
for {
|
||||
bindsWithoutSelfAlias <- topLevelValues.traverse(_.asBind)
|
||||
binds = bindsWithoutSelfAlias :+ selfAlias()
|
||||
_ <- validateNameUniqueness(binds)
|
||||
} yield binds.toIndexedSeq
|
||||
}
|
||||
|
||||
private def validateNameUniqueness(binds: Seq[Bind[_]]) = {
|
||||
val nonUniqueNames =
|
||||
binds.map(_.name).groupBy(identity).collect {
|
||||
case (name, occurrences) if occurrences.sizeIs > 1 =>
|
||||
s"$name (${occurrences.size} occurrences)"
|
||||
}
|
||||
EitherUtil.condUnitE(
|
||||
nonUniqueNames.isEmpty,
|
||||
new IllegalStateException(
|
||||
s"""Node names must be unique and must differ from reserved keywords. Please revisit node names in your config file.
|
||||
|Offending names: ${nonUniqueNames.mkString("(", ", ", ")")}""".stripMargin
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private def createParticipantReference(name: String): LocalParticipantReference =
|
||||
new LocalParticipantReference(this, name)
|
||||
private def createRemoteParticipantReference(name: String): RemoteParticipantReference =
|
||||
new RemoteParticipantReference(this, name)
|
||||
private def createParticipantReferenceX(name: String): LocalParticipantReferenceX =
|
||||
new LocalParticipantReferenceX(this, name)
|
||||
private def createRemoteParticipantReferenceX(name: String): RemoteParticipantReferenceX =
|
||||
new RemoteParticipantReferenceX(this, name)
|
||||
|
||||
protected def createDomainReference(name: String): DomainLocalRef
|
||||
protected def createRemoteDomainReference(name: String): DomainRemoteRef
|
||||
|
||||
/** So we can we make this available
|
||||
*/
|
||||
protected def selfAlias(): Bind[_] = Bind(ConsoleEnvironmentBinding.BindingName, this)
|
||||
|
||||
override def onClosed(): Unit = {
|
||||
Lifecycle.close(grpcAdminCommandRunner, environment)(logger)
|
||||
}
|
||||
|
||||
def closeChannels(): Unit = {
|
||||
grpcAdminCommandRunner.closeChannels()
|
||||
}
|
||||
|
||||
def startAll(): Unit = runE(environment.startAll())
|
||||
|
||||
def stopAll(): Unit = runE(environment.stopAll())
|
||||
|
||||
}
|
||||
|
||||
/** Expose a Canton [[environment.Environment]] in a way that's easy to deal with from a REPL.
|
||||
*/
|
||||
object ConsoleEnvironment {
|
||||
|
||||
trait Implicits {
|
||||
|
||||
import scala.language.implicitConversions
|
||||
|
||||
implicit def toInstanceReferenceExtensions(
|
||||
instances: Seq[LocalInstanceReferenceCommon]
|
||||
): LocalInstancesExtensions =
|
||||
new LocalInstancesExtensions.Impl(instances)
|
||||
|
||||
/** Implicit maps an LfPartyId to a PartyId */
|
||||
implicit def toPartId(lfPartyId: LfPartyId): PartyId = PartyId.tryFromLfParty(lfPartyId)
|
||||
|
||||
/** Extensions for many instance references
|
||||
*/
|
||||
implicit def toLocalDomainExtensions(
|
||||
instances: Seq[LocalDomainReference]
|
||||
): LocalInstancesExtensions =
|
||||
new LocalDomainReferencesExtensions(instances)
|
||||
|
||||
/** Extensions for many participant references
|
||||
*/
|
||||
implicit def toParticipantReferencesExtensions(participants: Seq[ParticipantReferenceCommon])(
|
||||
implicit consoleEnvironment: ConsoleEnvironment
|
||||
): ParticipantReferencesExtensions =
|
||||
new ParticipantReferencesExtensions(participants)
|
||||
|
||||
implicit def toLocalParticipantReferencesExtensions(
|
||||
participants: Seq[LocalParticipantReference]
|
||||
)(implicit consoleEnvironment: ConsoleEnvironment): LocalParticipantReferencesExtensions =
|
||||
new LocalParticipantReferencesExtensions(participants)
|
||||
|
||||
/** Implicitly map strings to DomainAlias, Fingerprint and Identifier
|
||||
*/
|
||||
implicit def toDomainAlias(alias: String): DomainAlias = DomainAlias.tryCreate(alias)
|
||||
implicit def toDomainAliases(aliases: Seq[String]): Seq[DomainAlias] =
|
||||
aliases.map(DomainAlias.tryCreate)
|
||||
|
||||
implicit def toInstanceName(name: String): InstanceName = InstanceName.tryCreate(name)
|
||||
|
||||
implicit def toGrpcSequencerConnection(connection: String): SequencerConnection =
|
||||
GrpcSequencerConnection.tryCreate(connection)
|
||||
|
||||
implicit def toSequencerConnections(connection: String): SequencerConnections =
|
||||
SequencerConnections.single(GrpcSequencerConnection.tryCreate(connection))
|
||||
|
||||
implicit def toGSequencerConnection(
|
||||
ref: InstanceReferenceWithSequencerConnection
|
||||
): SequencerConnection =
|
||||
ref.sequencerConnection
|
||||
|
||||
implicit def toGSequencerConnections(
|
||||
ref: InstanceReferenceWithSequencerConnection
|
||||
): SequencerConnections =
|
||||
SequencerConnections.single(ref.sequencerConnection)
|
||||
|
||||
implicit def toIdentifier(id: String): Identifier = Identifier.tryCreate(id)
|
||||
implicit def toFingerprint(fp: String): Fingerprint = Fingerprint.tryCreate(fp)
|
||||
|
||||
/** Implicitly map ParticipantReferences to the ParticipantId
|
||||
*/
|
||||
implicit def toParticipantId(reference: ParticipantReference): ParticipantId = reference.id
|
||||
implicit def toParticipantIdX(reference: ParticipantReferenceX): ParticipantId = reference.id
|
||||
|
||||
/** Implicitly map an `Int` to a `NonNegativeInt`.
|
||||
* @throws java.lang.IllegalArgumentException if `n` is negative
|
||||
*/
|
||||
implicit def toNonNegativeInt(n: Int): NonNegativeInt = NonNegativeInt.tryCreate(n)
|
||||
|
||||
/** Implicitly map an `Int` to a `PositiveInt`.
|
||||
* @throws java.lang.IllegalArgumentException if `n` is not positive
|
||||
*/
|
||||
implicit def toPositiveInt(n: Int): PositiveInt = PositiveInt.tryCreate(n)
|
||||
|
||||
/** Implicitly map a Double to a `PositiveDouble`
|
||||
* @throws java.lang.IllegalArgumentException if `n` is not positive
|
||||
*/
|
||||
implicit def toPositiveDouble(n: Double): PositiveDouble = PositiveDouble.tryCreate(n)
|
||||
|
||||
/** Implicitly map a `CantonTimestamp` to a `LedgerCreateTime`
|
||||
*/
|
||||
implicit def toLedgerCreateTime(ts: CantonTimestamp): SerializableContract.LedgerCreateTime =
|
||||
SerializableContract.LedgerCreateTime(ts)
|
||||
|
||||
/** Implicitly convert a duration to a [[com.digitalasset.canton.config.NonNegativeDuration]]
|
||||
* @throws java.lang.IllegalArgumentException if `duration` is negative
|
||||
*/
|
||||
implicit def durationToNonNegativeDuration(duration: SDuration): NonNegativeDuration =
|
||||
NonNegativeDuration.tryFromDuration(duration)
|
||||
|
||||
/** Implicitly convert a duration to a [[com.digitalasset.canton.config.NonNegativeFiniteDuration]]
|
||||
* @throws java.lang.IllegalArgumentException if `duration` is negative or infinite
|
||||
*/
|
||||
implicit def durationToNonNegativeFiniteDuration(
|
||||
duration: SDuration
|
||||
): NonNegativeFiniteDuration =
|
||||
NonNegativeFiniteDuration.tryFromDuration(duration)
|
||||
|
||||
/** Implicitly convert a duration to a [[com.digitalasset.canton.config.PositiveDurationSeconds]]
|
||||
*
|
||||
* @throws java.lang.IllegalArgumentException if `duration` is not positive or not rounded to the second.
|
||||
*/
|
||||
implicit def durationToPositiveDurationRoundedSeconds(
|
||||
duration: SDuration
|
||||
): PositiveDurationSeconds =
|
||||
PositiveDurationSeconds.tryFromDuration(duration)
|
||||
}
|
||||
|
||||
object Implicits extends Implicits
|
||||
|
||||
}
|
||||
|
||||
class SimClockCommand(clock: SimClock) {
|
||||
|
||||
@Help.Description("Get current time")
|
||||
def now: Instant = clock.now.toInstant
|
||||
|
||||
@Help.Description("Advance time to given time-point")
|
||||
def advanceTo(timestamp: Instant): Unit = TraceContext.withNewTraceContext {
|
||||
implicit traceContext =>
|
||||
clock.advanceTo(CantonTimestamp.assertFromInstant(timestamp))
|
||||
}
|
||||
|
||||
@Help.Description("Advance time by given time-period")
|
||||
def advance(duration: JDuration): Unit = TraceContext.withNewTraceContext {
|
||||
implicit traceContext =>
|
||||
clock.advance(duration)
|
||||
}
|
||||
|
||||
@Help.Summary("Reset simulation clock")
|
||||
def reset(): Unit = clock.reset()
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
/** To make the [[ConsoleEnvironment]] functionality conveniently available in ammonite we stash
|
||||
* it in a implicit variable included as a predef before any script or REPL commands are run.
|
||||
*/
|
||||
class ConsoleEnvironmentBinding {
|
||||
|
||||
protected def consoleMacrosImport: String =
|
||||
"import com.digitalasset.canton.console.ConsoleMacros._"
|
||||
|
||||
/** The predef code itself which is executed before any script or repl command */
|
||||
def predefCode(interactive: Boolean, noTty: Boolean = false): String = {
|
||||
val consoleEnvClassName = objectClassNameWithoutSuffix(ConsoleEnvironment.Implicits.getClass)
|
||||
|
||||
// this is the magic which allows us to use extensions such as `all start` on a sequence of instance references
|
||||
// and those extensions to still obtain an implicit reference to the [[ConsoleEnvironment]] instance (where state like packages is kept)
|
||||
val builder = new StringBuilder(s"""
|
||||
|interp.configureCompiler(_.settings.processArgumentString("-Xsource:2.13"))
|
||||
|import $consoleEnvClassName._
|
||||
|import com.digitalasset.canton.topology.store.TimeQuery
|
||||
|import com.digitalasset.canton.topology._
|
||||
|import com.digitalasset.canton.topology.transaction._
|
||||
|import com.digitalasset.canton.crypto._
|
||||
|import com.digitalasset.canton.config._
|
||||
|import com.digitalasset.canton.admin.api.client.data._
|
||||
|import com.digitalasset.canton.participant.domain.DomainConnectionConfig
|
||||
|import com.digitalasset.canton.SequencerAlias
|
||||
|import com.digitalasset.canton.sequencing.SequencerConnection
|
||||
|import com.digitalasset.canton.sequencing.SequencerConnections
|
||||
|import com.digitalasset.canton.sequencing.GrpcSequencerConnection
|
||||
|$consoleMacrosImport
|
||||
|import com.digitalasset.canton.console.commands.DomainChoice
|
||||
|import ${classOf[com.digitalasset.canton.console.BootstrapScriptException].getName}
|
||||
|import com.digitalasset.canton.config.RequireTypes._
|
||||
|import com.digitalasset.canton.participant.admin.ResourceLimits
|
||||
|import java.time.Instant
|
||||
|import scala.concurrent.ExecutionContextExecutor
|
||||
|import scala.concurrent.duration._
|
||||
|import scala.language.postfixOps
|
||||
|implicit val consoleEnvironment = ${ConsoleEnvironmentBinding.BindingName}
|
||||
|implicit val ec: ExecutionContextExecutor = consoleEnvironment.environment.executionContext
|
||||
|implicit def fromSequencerConnection(connection: SequencerConnection): SequencerConnections =
|
||||
| SequencerConnections.single(connection)
|
||||
|def help = consoleEnvironment.help
|
||||
|def help(s: String) = consoleEnvironment.help(s)
|
||||
|def health = consoleEnvironment.health
|
||||
|def logger = consoleEnvironment.consoleLogger
|
||||
""".stripMargin)
|
||||
|
||||
// if we don't have a tty available switch the ammonite frontend to a dumb terminal
|
||||
if (noTty) {
|
||||
builder ++= System.lineSeparator()
|
||||
builder ++= "repl.frontEnd() = new ammonite.repl.FrontEnds.JLineUnix(ammonite.compiler.Parsers)"
|
||||
}
|
||||
|
||||
if (interactive) {
|
||||
builder ++= System.lineSeparator()
|
||||
builder ++= "repl.pprinter() = repl.pprinter().copy(additionalHandlers = { case p: com.digitalasset.canton.logging.pretty.PrettyPrinting => import com.digitalasset.canton.logging.pretty.Pretty._; p.toTree }, defaultHeight = 100)"
|
||||
}
|
||||
|
||||
builder.result()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ConsoleEnvironmentBinding {
|
||||
|
||||
/** where we hide the value of the active environment instance within the scope of our repl ******* */
|
||||
private[console] val BindingName = "__replEnvironmentValue"
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import scala.util.control.NoStackTrace
|
||||
|
||||
/** Handle an error from a console.
|
||||
* We expect this implementation will either throw or exit, hence the [[scala.Nothing]] return type.
|
||||
*/
|
||||
trait ConsoleErrorHandler {
|
||||
def handleCommandFailure(): Nothing
|
||||
|
||||
def handleInternalError(): Nothing
|
||||
}
|
||||
|
||||
final class CommandFailure() extends Throwable("Command execution failed.") with NoStackTrace
|
||||
|
||||
final class CantonInternalError()
|
||||
extends Throwable(
|
||||
"Command execution failed due to an internal error. Please file a bug report."
|
||||
)
|
||||
with NoStackTrace
|
||||
|
||||
/** Throws a [[CommandFailure]] or [[CantonInternalError]] when a command fails.
|
||||
* The throwables do not have a stacktraces, to avoid noise in the interactive console.
|
||||
*/
|
||||
object ThrowErrorHandler extends ConsoleErrorHandler {
|
||||
override def handleCommandFailure(): Nothing = throw new CommandFailure()
|
||||
|
||||
override def handleInternalError(): Nothing = throw new CantonInternalError()
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.GrpcCtlRunner
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand.{
|
||||
CustomClientTimeout,
|
||||
DefaultBoundedTimeout,
|
||||
DefaultUnboundedTimeout,
|
||||
ServerEnforcedTimeout,
|
||||
}
|
||||
import com.digitalasset.canton.config.RequireTypes.Port
|
||||
import com.digitalasset.canton.config.{ClientConfig, ConsoleCommandTimeout, NonNegativeDuration}
|
||||
import com.digitalasset.canton.environment.Environment
|
||||
import com.digitalasset.canton.lifecycle.Lifecycle.CloseableChannel
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.networking.grpc.ClientChannelBuilder
|
||||
import com.digitalasset.canton.tracing.{Spanning, TraceContext}
|
||||
import io.opentelemetry.api.trace.Tracer
|
||||
|
||||
import java.util.concurrent.TimeUnit
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.concurrent.duration.{Duration, FiniteDuration}
|
||||
import scala.concurrent.{ExecutionContextExecutor, blocking}
|
||||
|
||||
/** Attempt to run a grpc admin-api command against whatever is pointed at in the config
|
||||
*/
|
||||
class GrpcAdminCommandRunner(
|
||||
environment: Environment,
|
||||
val commandTimeouts: ConsoleCommandTimeout,
|
||||
)(implicit tracer: Tracer)
|
||||
extends NamedLogging
|
||||
with AutoCloseable
|
||||
with Spanning {
|
||||
|
||||
private implicit val executionContext: ExecutionContextExecutor =
|
||||
environment.executionContext
|
||||
override val loggerFactory: NamedLoggerFactory = environment.loggerFactory
|
||||
|
||||
private val grpcRunner = new GrpcCtlRunner(
|
||||
environment.config.monitoring.logging.api.maxMessageLines,
|
||||
environment.config.monitoring.logging.api.maxStringLength,
|
||||
loggerFactory,
|
||||
)
|
||||
private val channels = TrieMap[(String, String, Port), CloseableChannel]()
|
||||
|
||||
def runCommandAsync[Result](
|
||||
instanceName: String,
|
||||
command: GrpcAdminCommand[_, _, Result],
|
||||
clientConfig: ClientConfig,
|
||||
token: Option[String],
|
||||
)(implicit traceContext: TraceContext) = {
|
||||
val awaitTimeout = command.timeoutType match {
|
||||
case CustomClientTimeout(timeout) => timeout
|
||||
// If a custom timeout for a console command is set, it involves some non-gRPC timeout mechanism
|
||||
// -> we set the gRPC timeout to Inf, so gRPC never times out before the other timeout mechanism
|
||||
case ServerEnforcedTimeout => NonNegativeDuration(Duration.Inf)
|
||||
case DefaultBoundedTimeout => commandTimeouts.bounded
|
||||
case DefaultUnboundedTimeout => commandTimeouts.unbounded
|
||||
}
|
||||
val callTimeout = awaitTimeout.duration match {
|
||||
// Abort the command shortly before the console times out, to get a better error message
|
||||
case x: FiniteDuration => Duration((x.toMillis * 9) / 10, TimeUnit.MILLISECONDS)
|
||||
case x => x
|
||||
}
|
||||
val closeableChannel = getOrCreateChannel(instanceName, clientConfig)
|
||||
logger.debug(s"Running on ${instanceName} command ${command} against ${clientConfig}")(
|
||||
traceContext
|
||||
)
|
||||
(
|
||||
awaitTimeout,
|
||||
grpcRunner.run(instanceName, command, closeableChannel.channel, token, callTimeout),
|
||||
)
|
||||
}
|
||||
|
||||
def runCommand[Result](
|
||||
instanceName: String,
|
||||
command: GrpcAdminCommand[_, _, Result],
|
||||
clientConfig: ClientConfig,
|
||||
token: Option[String],
|
||||
): ConsoleCommandResult[Result] =
|
||||
withNewTrace[ConsoleCommandResult[Result]](command.fullName) { implicit traceContext => span =>
|
||||
span.setAttribute("instance_name", instanceName)
|
||||
val (awaitTimeout, commandET) = runCommandAsync(instanceName, command, clientConfig, token)
|
||||
val apiResult =
|
||||
awaitTimeout.await(
|
||||
s"Running on ${instanceName} command ${command} against ${clientConfig}"
|
||||
)(
|
||||
commandET.value
|
||||
)
|
||||
// convert to a console command result
|
||||
apiResult.toResult
|
||||
}
|
||||
|
||||
private def getOrCreateChannel(
|
||||
instanceName: String,
|
||||
clientConfig: ClientConfig,
|
||||
): CloseableChannel =
|
||||
blocking(synchronized {
|
||||
val addr = (instanceName, clientConfig.address, clientConfig.port)
|
||||
channels.getOrElseUpdate(
|
||||
addr,
|
||||
new CloseableChannel(
|
||||
ClientChannelBuilder.createChannelToTrustedServer(clientConfig),
|
||||
logger,
|
||||
s"ConsoleCommand",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
override def close(): Unit = {
|
||||
closeChannels()
|
||||
}
|
||||
|
||||
def closeChannels(): Unit = {
|
||||
channels.values.foreach(_.close())
|
||||
channels.clear()
|
||||
}
|
||||
}
|
||||
|
||||
class ConsoleGrpcAdminCommandRunner(consoleEnvironment: ConsoleEnvironment)
|
||||
extends GrpcAdminCommandRunner(
|
||||
consoleEnvironment.environment,
|
||||
consoleEnvironment.commandTimeouts,
|
||||
)(consoleEnvironment.tracer)
|
@ -0,0 +1,847 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import better.files.File
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.functor.*
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||
import ch.qos.logback.classic.{Level, Logger}
|
||||
import ch.qos.logback.core.spi.AppenderAttachable
|
||||
import ch.qos.logback.core.{Appender, FileAppender}
|
||||
import com.daml.ledger.api.v1.commands.{Command, CreateCommand, ExerciseCommand}
|
||||
import com.daml.ledger.api.v1.event.CreatedEvent
|
||||
import com.daml.ledger.api.v1.value.Value.Sum
|
||||
import com.daml.ledger.api.v1.value.{
|
||||
Identifier as IdentifierV1,
|
||||
List as ListV1,
|
||||
Optional,
|
||||
Record,
|
||||
RecordField,
|
||||
Value,
|
||||
}
|
||||
import com.daml.lf.value.Value.ContractId
|
||||
import com.digitalasset.canton.DomainAlias
|
||||
import com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.ContractData
|
||||
import com.digitalasset.canton.admin.api.client.data.{ListPartiesResult, TemplateId}
|
||||
import com.digitalasset.canton.concurrent.Threading
|
||||
import com.digitalasset.canton.config.NonNegativeDuration
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.console.ConsoleEnvironment.Implicits.*
|
||||
import com.digitalasset.canton.crypto.{CryptoPureApi, Salt}
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.logging.{LastErrorsAppender, NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.participant.admin.inspection.SyncStateInspection
|
||||
import com.digitalasset.canton.participant.admin.repair.RepairService
|
||||
import com.digitalasset.canton.participant.config.{AuthServiceConfig, BaseParticipantConfig}
|
||||
import com.digitalasset.canton.participant.ledger.api.JwtTokenUtilities
|
||||
import com.digitalasset.canton.protocol.SerializableContract.LedgerCreateTime
|
||||
import com.digitalasset.canton.protocol.*
|
||||
import com.digitalasset.canton.topology.*
|
||||
import com.digitalasset.canton.tracing.{NoTracing, TraceContext}
|
||||
import com.digitalasset.canton.util.BinaryFileUtil
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
import com.google.protobuf.ByteString
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import io.circe.Encoder
|
||||
import io.circe.generic.semiauto.deriveEncoder
|
||||
import io.circe.syntax.*
|
||||
|
||||
import java.io.File as JFile
|
||||
import java.time.Instant
|
||||
import scala.annotation.nowarn
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.duration.*
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
trait ConsoleMacros extends NamedLogging with NoTracing {
|
||||
import scala.reflect.runtime.universe.*
|
||||
|
||||
@Help.Summary("Console utilities")
|
||||
@Help.Group("Utilities")
|
||||
object utils extends Helpful {
|
||||
|
||||
@Help.Summary("Reflective inspection of object arguments, handy to inspect case class objects")
|
||||
@Help.Description(
|
||||
"Return the list field names of the given object. Helpful function when inspecting the return result."
|
||||
)
|
||||
def object_args[T: TypeTag](obj: T): List[String] = type_args[T]
|
||||
|
||||
@Help.Summary("Reflective inspection of type arguments, handy to inspect case class types")
|
||||
@Help.Description(
|
||||
"Return the list of field names of the given type. Helpful function when creating new objects for requests."
|
||||
)
|
||||
def type_args[T: TypeTag]: List[String] =
|
||||
typeOf[T].members.collect {
|
||||
case m: MethodSymbol if m.isCaseAccessor => s"${m.name}:${m.returnType}"
|
||||
}.toList
|
||||
|
||||
@Help.Summary("Wait for a condition to become true, using default timeouts")
|
||||
@Help.Description("""
|
||||
|Wait until condition becomes true, with a timeout taken from the parameters.timeouts.console.bounded
|
||||
|configuration parameter.""")
|
||||
final def retry_until_true(
|
||||
condition: => Boolean
|
||||
)(implicit
|
||||
env: ConsoleEnvironment
|
||||
): Unit = retry_until_true(env.commandTimeouts.bounded)(
|
||||
condition,
|
||||
s"Condition never became true within ${env.commandTimeouts.bounded.unwrap}",
|
||||
)
|
||||
|
||||
@Help.Summary("Wait for a condition to become true")
|
||||
@Help.Description("""Wait `timeout` duration until `condition` becomes true.
|
||||
| Retry evaluating `condition` with an exponentially increasing back-off up to `maxWaitPeriod` duration between retries.
|
||||
|""")
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Var", "org.wartremover.warts.While"))
|
||||
final def retry_until_true(
|
||||
timeout: NonNegativeDuration,
|
||||
maxWaitPeriod: NonNegativeDuration = 10.seconds,
|
||||
)(
|
||||
condition: => Boolean,
|
||||
failure: => String = s"Condition never became true within $timeout",
|
||||
): Unit = {
|
||||
val deadline = timeout.asFiniteApproximation.fromNow
|
||||
var isCompleted = condition
|
||||
var waitMillis = 1L
|
||||
while (!isCompleted) {
|
||||
val timeLeft = deadline.timeLeft
|
||||
if (timeLeft > Duration.Zero) {
|
||||
val remaining = (timeLeft min (waitMillis.millis)) max 1.millis
|
||||
Threading.sleep(remaining.toMillis)
|
||||
// capped exponentially back off
|
||||
waitMillis = (waitMillis * 2) min maxWaitPeriod.duration.toMillis
|
||||
isCompleted = condition
|
||||
} else {
|
||||
throw new IllegalStateException(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Wait until all topology changes have been effected on all accessible nodes")
|
||||
def synchronize_topology(
|
||||
timeoutO: Option[NonNegativeDuration] = None
|
||||
)(implicit env: ConsoleEnvironment): Unit = {
|
||||
ConsoleMacros.utils.retry_until_true(timeoutO.getOrElse(env.commandTimeouts.bounded)) {
|
||||
env.nodes.all.forall(_.topology.synchronisation.is_idle())
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Create a navigator ui-backend.conf for a participant")
|
||||
def generate_navigator_conf(
|
||||
participant: LocalParticipantReference,
|
||||
file: Option[String] = None,
|
||||
): JFile = {
|
||||
val conf =
|
||||
participant.parties
|
||||
.hosted()
|
||||
.map(x => x.party)
|
||||
.map(party => {
|
||||
s""" ${party.uid.id.unwrap} {
|
||||
| party = "${party.uid.toProtoPrimitive}"
|
||||
| password = password
|
||||
| }
|
||||
|""".stripMargin
|
||||
})
|
||||
.mkString("\n")
|
||||
val port = participant.config.ledgerApi.port
|
||||
val targetFile = file.map(File(_)).getOrElse(File(s"ui-backend-${participant.name}.conf"))
|
||||
val instructions =
|
||||
s"daml navigator server localhost ${port.unwrap} -t wallclock --port ${(port + 4).unwrap.toString} -c ${targetFile.name}"
|
||||
|
||||
targetFile.overwrite("// run with\n// ")
|
||||
targetFile.appendLines(instructions, "users {")
|
||||
targetFile.appendText(conf)
|
||||
targetFile.appendLine("}")
|
||||
|
||||
targetFile.toJava
|
||||
}
|
||||
|
||||
@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072
|
||||
private object GenerateDamlScriptParticipantsConf {
|
||||
import ConsoleEnvironment.Implicits.*
|
||||
|
||||
private val filename = "participant-config.json"
|
||||
|
||||
case class LedgerApi(host: String, port: Int)
|
||||
// Keys in the exported JSON should have snake_case
|
||||
case class Participants(
|
||||
default_participant: Option[LedgerApi],
|
||||
participants: Map[String, LedgerApi],
|
||||
party_participants: Map[String, String],
|
||||
)
|
||||
|
||||
implicit val ledgerApiEncoder: Encoder[LedgerApi] = deriveEncoder[LedgerApi]
|
||||
implicit val participantsEncoder: Encoder[Participants] = deriveEncoder[Participants]
|
||||
|
||||
private def partyIdToParticipants(
|
||||
useParticipantAlias: Boolean,
|
||||
uidToAlias: Map[ParticipantId, String],
|
||||
)(implicit env: ConsoleEnvironment): Map[String, String] = {
|
||||
def participantReference(p: ParticipantId) = if (useParticipantAlias)
|
||||
uidToAlias.getOrElse(p, p.uid.toProtoPrimitive)
|
||||
else p.uid.toProtoPrimitive
|
||||
def partyIdToParticipant(p: ListPartiesResult) = p.participants.headOption.map {
|
||||
participantDomains =>
|
||||
(p.party.filterString, participantReference(participantDomains.participant))
|
||||
}
|
||||
|
||||
val partyAndParticipants =
|
||||
env.participants.all.flatMap(_.parties.list().flatMap(partyIdToParticipant(_).toList))
|
||||
val allPartiesSingleParticipant =
|
||||
partyAndParticipants.groupBy { case (partyId, _) => partyId }.forall {
|
||||
case (_, participants) => participants.sizeCompare(1) <= 0
|
||||
}
|
||||
|
||||
if (!allPartiesSingleParticipant)
|
||||
logger.info(
|
||||
"Some parties are hosted on more than one participant. " +
|
||||
"For such parties, only one participant will be exported to the generated config file."
|
||||
)
|
||||
|
||||
partyAndParticipants.toMap
|
||||
}
|
||||
|
||||
def apply(
|
||||
file: Option[String] = None,
|
||||
useParticipantAlias: Boolean = true,
|
||||
defaultParticipant: Option[ParticipantReference] = None,
|
||||
)(implicit env: ConsoleEnvironment): JFile = {
|
||||
|
||||
def toLedgerApi(participantConfig: BaseParticipantConfig) =
|
||||
LedgerApi(
|
||||
participantConfig.clientLedgerApi.address,
|
||||
participantConfig.clientLedgerApi.port.unwrap,
|
||||
)
|
||||
|
||||
def participantValue(p: ParticipantReference): String =
|
||||
if (useParticipantAlias) p.name else p.uid.toProtoPrimitive
|
||||
|
||||
val allParticipants = env.participants.all
|
||||
val participantsData =
|
||||
allParticipants.map(p => (participantValue(p), toLedgerApi(p.config))).toMap
|
||||
val uidToAlias = allParticipants.map(p => (p.id, p.name)).toMap
|
||||
|
||||
val default_participant =
|
||||
defaultParticipant.map(participantReference => toLedgerApi(participantReference.config))
|
||||
|
||||
val participantJson = Participants(
|
||||
default_participant,
|
||||
participantsData,
|
||||
partyIdToParticipants(useParticipantAlias, uidToAlias),
|
||||
).asJson.spaces2
|
||||
|
||||
val targetFile = file.map(File(_)).getOrElse(File(filename))
|
||||
targetFile.overwrite(participantJson).appendLine()
|
||||
|
||||
targetFile.toJava
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Create a participants config for Daml script")
|
||||
@Help.Description(
|
||||
"""The generated config can be passed to `daml script` via the `participant-config` parameter.
|
||||
|More information about the file format can be found in the `documentation <https://docs.daml.com/daml-script/index.html#using-daml-script-in-distributed-topologies>`_:
|
||||
|It takes three arguments:
|
||||
|- file (default to "participant-config.json")
|
||||
|- useParticipantAlias (default to true): participant aliases are used instead of UIDs
|
||||
|- defaultParticipant (default to None): adds a default participant if provided
|
||||
|"""
|
||||
)
|
||||
def generate_daml_script_participants_conf(
|
||||
file: Option[String] = None,
|
||||
useParticipantAlias: Boolean = true,
|
||||
defaultParticipant: Option[ParticipantReference] = None,
|
||||
)(implicit env: ConsoleEnvironment): JFile =
|
||||
GenerateDamlScriptParticipantsConf(
|
||||
file,
|
||||
useParticipantAlias,
|
||||
defaultParticipant,
|
||||
)
|
||||
|
||||
// TODO(i7387): add check that flag is set
|
||||
@Help.Summary(
|
||||
"Register `AutoCloseable` object to be shutdown if Canton is shut down",
|
||||
FeatureFlag.Testing,
|
||||
)
|
||||
def auto_close(closeable: AutoCloseable)(implicit environment: ConsoleEnvironment): Unit = {
|
||||
environment.environment.addUserCloseable(closeable)
|
||||
}
|
||||
|
||||
@Help.Summary("Convert contract data to a contract instance.")
|
||||
@Help.Description(
|
||||
"""The `utils.contract_data_to_instance` bridges the gap between `participant.ledger_api.acs` commands that
|
||||
|return various pieces of "contract data" and the `participant.repair.add` command used to add "contract instances"
|
||||
|as part of repair workflows. Such workflows (for example migrating contracts from other Daml ledgers to Canton
|
||||
|participants) typically consist of extracting contract data using `participant.ledger_api.acs` commands,
|
||||
|modifying the contract data, and then converting the `contractData` using this function before finally
|
||||
|adding the resulting contract instances to Canton participants via `participant.repair.add`.
|
||||
|Obtain the `contractData` by invoking `.toContractData` on the `WrappedCreatedEvent` returned by the
|
||||
|corresponding `participant.ledger_api.acs.of_party` or `of_all` call. The `ledgerTime` parameter should be
|
||||
|chosen to be a time meaningful to the domain on which you plan to subsequently invoke `participant.repair.add`
|
||||
|on and will be retained alongside the contract instance by the `participant.repair.add` invocation."""
|
||||
)
|
||||
def contract_data_to_instance(contractData: ContractData, ledgerTime: Instant)(implicit
|
||||
env: ConsoleEnvironment
|
||||
): SerializableContract =
|
||||
TraceContext.withNewTraceContext { implicit traceContext =>
|
||||
env.runE(
|
||||
RepairService.ContractConverter.contractDataToInstance(
|
||||
contractData.templateId.toIdentifier,
|
||||
contractData.createArguments,
|
||||
contractData.signatories,
|
||||
contractData.observers,
|
||||
contractData.inheritedContractId,
|
||||
contractData.ledgerCreateTime.map(_.toInstant).getOrElse(ledgerTime),
|
||||
contractData.contractSalt,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Convert a contract instance to contract data.")
|
||||
@Help.Description(
|
||||
"""The `utils.contract_instance_to_data` converts a Canton "contract instance" to "contract data", a format more
|
||||
|amenable to inspection and modification as part of repair workflows. This function consumes the output of
|
||||
|the `participant.testing` commands and can thus be employed in workflows geared at verifying the contents of
|
||||
|contracts for diagnostic purposes and in environments in which the "features.enable-testing-commands"
|
||||
|configuration can be (at least temporarily) enabled."""
|
||||
)
|
||||
def contract_instance_to_data(
|
||||
contract: SerializableContract
|
||||
)(implicit env: ConsoleEnvironment): ContractData =
|
||||
env.runE(
|
||||
RepairService.ContractConverter.contractInstanceToData(contract).map {
|
||||
case (
|
||||
templateId,
|
||||
createArguments,
|
||||
signatories,
|
||||
observers,
|
||||
contractId,
|
||||
contractSaltO,
|
||||
ledgerCreateTime,
|
||||
) =>
|
||||
ContractData(
|
||||
TemplateId.fromIdentifier(templateId),
|
||||
createArguments,
|
||||
signatories,
|
||||
observers,
|
||||
contractId,
|
||||
contractSaltO,
|
||||
Some(ledgerCreateTime.ts.underlying),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@Help.Summary("Recompute authenticated contract ids.")
|
||||
@Help.Description(
|
||||
"""The `utils.recompute_contract_ids` regenerates "contract ids" of multiple contracts after their contents have
|
||||
|changed. Starting from protocol version 4, Canton uses the so called authenticated contract ids which depend
|
||||
|on the details of the associated contracts. When aspects of a contract such as the parties involved change as
|
||||
|part of repair or export/import procedure, the corresponding contract id must be recomputed."""
|
||||
)
|
||||
def recompute_contract_ids(
|
||||
participant: LocalParticipantReference,
|
||||
acs: Seq[SerializableContract],
|
||||
protocolVersion: ProtocolVersion,
|
||||
): (Seq[SerializableContract], Map[LfContractId, LfContractId]) = {
|
||||
val contractIdMappings = mutable.Map.empty[LfContractId, LfContractId]
|
||||
// We assume ACS events are in order
|
||||
val remappedCIds = acs.map { contract =>
|
||||
// Update the referenced contract ids
|
||||
val contractInstanceWithUpdatedContractIdReferences =
|
||||
SerializableRawContractInstance
|
||||
.create(
|
||||
contract.rawContractInstance.contractInstance.map(_.mapCid(contractIdMappings)),
|
||||
AgreementText.empty, // Empty is fine, because the agreement text is not used when generating the raw serializable contract hash
|
||||
)
|
||||
.valueOr(err =>
|
||||
throw new RuntimeException(
|
||||
s"Could not create serializable raw contract instance: $err"
|
||||
)
|
||||
)
|
||||
|
||||
val LfContractId.V1(discriminator, _) = contract.contractId
|
||||
val contractSalt = contract.contractSalt.getOrElse(
|
||||
throw new IllegalArgumentException("Missing contract salt")
|
||||
)
|
||||
val pureCrypto = participant.underlying
|
||||
.map(_.cryptoPureApi)
|
||||
.getOrElse(sys.error("where is my crypto?"))
|
||||
|
||||
// Compute the new contract id
|
||||
val newContractId =
|
||||
generate_contract_id(
|
||||
cryptoPureApi = pureCrypto,
|
||||
rawContract = contractInstanceWithUpdatedContractIdReferences,
|
||||
createdAt = contract.ledgerCreateTime.ts,
|
||||
discriminator = discriminator,
|
||||
contractSalt = contractSalt,
|
||||
metadata = contract.metadata,
|
||||
)
|
||||
|
||||
// Update the contract id mappings with the current contract's id
|
||||
contractIdMappings += contract.contractId -> newContractId
|
||||
|
||||
// Update the contract with the new contract id and recomputed instance
|
||||
contract
|
||||
.copy(
|
||||
contractId = newContractId,
|
||||
rawContractInstance = contractInstanceWithUpdatedContractIdReferences,
|
||||
)
|
||||
}
|
||||
|
||||
remappedCIds -> Map.from(contractIdMappings)
|
||||
}
|
||||
|
||||
@Help.Summary("Generate authenticated contract id.")
|
||||
@Help.Description(
|
||||
"""The `utils.generate_contract_id` generates "contract id" of a contract. Starting from protocol version 4,
|
||||
|Canton uses the so called authenticated contract ids which depend on the details of the associated contracts.
|
||||
|When aspects of a contract such as the parties involved change as part of repair or export/import procedure,
|
||||
|the corresponding contract id must be recomputed. This function can be used as a tool to generate an id for
|
||||
|an arbitrary contract content"""
|
||||
)
|
||||
def generate_contract_id(
|
||||
cryptoPureApi: CryptoPureApi,
|
||||
rawContract: SerializableRawContractInstance,
|
||||
createdAt: CantonTimestamp,
|
||||
discriminator: LfHash,
|
||||
contractSalt: Salt,
|
||||
metadata: ContractMetadata,
|
||||
): ContractId.V1 = {
|
||||
val unicumGenerator = new UnicumGenerator(cryptoPureApi)
|
||||
val cantonContractIdVersion = AuthenticatedContractIdVersionV2
|
||||
val unicum = unicumGenerator
|
||||
.recomputeUnicum(
|
||||
contractSalt,
|
||||
LedgerCreateTime(createdAt),
|
||||
metadata,
|
||||
rawContract,
|
||||
cantonContractIdVersion,
|
||||
)
|
||||
.valueOr(err => throw new RuntimeException(err))
|
||||
cantonContractIdVersion.fromDiscriminator(discriminator, unicum)
|
||||
}
|
||||
|
||||
@Help.Summary("Writes several Protobuf messages to a file.")
|
||||
def write_to_file(data: Seq[scalapb.GeneratedMessage], fileName: String): Unit =
|
||||
File(fileName).outputStream.foreach { os =>
|
||||
data.foreach(_.writeDelimitedTo(os))
|
||||
}
|
||||
|
||||
@Help.Summary("Reads several Protobuf messages from a file.")
|
||||
@Help.Description("Fails with an exception, if the file can't be read or parsed.")
|
||||
def read_all_messages_from_file[A <: scalapb.GeneratedMessage](
|
||||
fileName: String
|
||||
)(implicit companion: scalapb.GeneratedMessageCompanion[A]): Seq[A] =
|
||||
File(fileName).inputStream
|
||||
.apply { is =>
|
||||
Seq.unfold(()) { _ =>
|
||||
companion.parseDelimitedFrom(is).map(_ -> ())
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Writes a Protobuf message to a file.")
|
||||
def write_to_file(data: scalapb.GeneratedMessage, fileName: String): Unit =
|
||||
write_to_file(Seq(data), fileName)
|
||||
|
||||
@Help.Summary("Reads a single Protobuf message from a file.")
|
||||
@Help.Description("Fails with an exception, if the file can't be read or parsed.")
|
||||
def read_first_message_from_file[A <: scalapb.GeneratedMessage](
|
||||
fileName: String
|
||||
)(implicit companion: scalapb.GeneratedMessageCompanion[A]): A =
|
||||
File(fileName).inputStream
|
||||
.apply(companion.parseDelimitedFrom)
|
||||
.getOrElse(
|
||||
throw new IllegalArgumentException(
|
||||
s"Unable to read ${companion.getClass.getSimpleName} from $fileName."
|
||||
)
|
||||
)
|
||||
|
||||
@Help.Summary("Writes a ByteString to a file.")
|
||||
def write_to_file(data: ByteString, fileName: String): Unit =
|
||||
BinaryFileUtil.writeByteStringToFile(fileName, data)
|
||||
|
||||
@Help.Summary("Reads a ByteString from a file.")
|
||||
@Help.Description("Fails with an exception, if the file can't be read.")
|
||||
def read_byte_string_from_file(fileName: String)(implicit env: ConsoleEnvironment): ByteString =
|
||||
env.runE(BinaryFileUtil.readByteStringFromFile(fileName))
|
||||
|
||||
}
|
||||
|
||||
@Help.Summary("Canton development and testing utilities", FeatureFlag.Testing)
|
||||
@Help.Group("Ledger Api Testing")
|
||||
object ledger_api_utils extends Helpful {
|
||||
|
||||
private def buildIdentifier(packageId: String, module: String, template: String): IdentifierV1 =
|
||||
IdentifierV1(
|
||||
packageId = packageId,
|
||||
moduleName = module,
|
||||
entityName = template,
|
||||
)
|
||||
|
||||
private def productToLedgerApiRecord(product: Product): Sum.Record = Value.Sum.Record(
|
||||
Record(fields =
|
||||
product.productIterator
|
||||
.map(mapToLedgerApiValue)
|
||||
.map(v => RecordField(value = Some(v)))
|
||||
.toSeq
|
||||
)
|
||||
)
|
||||
|
||||
private def mapToLedgerApiValue(value: Any): Value = {
|
||||
|
||||
// assuming that String.toString = id, we'll just map any Map to a string map without casting
|
||||
def safeMapCast(map: Map[_, _]): Map[String, Any] = map.map { case (key, value) =>
|
||||
(key.toString, value)
|
||||
}
|
||||
|
||||
val x: Value.Sum = value match {
|
||||
case x: Int => Value.Sum.Int64(x.toLong)
|
||||
case x: Long => Value.Sum.Int64(x)
|
||||
case x: PartyId => Value.Sum.Party(x.toLf)
|
||||
case x: Float => Value.Sum.Numeric(s"$x")
|
||||
case x: Double => Value.Sum.Numeric(s"$x")
|
||||
case x: String => Value.Sum.Text(x)
|
||||
case x: Boolean => Value.Sum.Bool(x)
|
||||
case x: Seq[Any] => Value.Sum.List(value = ListV1(x.map(mapToLedgerApiValue)))
|
||||
case x: LfContractId => Value.Sum.ContractId(x.coid)
|
||||
case x: Instant => Value.Sum.Timestamp(x.toEpochMilli * 1000L)
|
||||
case x: Option[Any] => Value.Sum.Optional(Optional(value = x.map(mapToLedgerApiValue)))
|
||||
case x: Value.Sum => x
|
||||
case x: Map[_, _] => Value.Sum.Record(buildArguments(safeMapCast(x)))
|
||||
case x: (Any, Any) => productToLedgerApiRecord(x)
|
||||
case x: (Any, Any, Any) => productToLedgerApiRecord(x)
|
||||
case _ =>
|
||||
throw new UnsupportedOperationException(
|
||||
s"value type not yet implemented: ${value.getClass}"
|
||||
)
|
||||
}
|
||||
Value(x)
|
||||
}
|
||||
|
||||
private def mapToRecordField(item: (String, Any)): RecordField =
|
||||
RecordField(
|
||||
label = item._1,
|
||||
value = Some(mapToLedgerApiValue(item._2)),
|
||||
)
|
||||
|
||||
private def buildArguments(map: Map[String, Any]): Record =
|
||||
Record(
|
||||
fields = map.map(mapToRecordField).toSeq
|
||||
)
|
||||
|
||||
@Help.Summary("Build create command", FeatureFlag.Testing)
|
||||
def create(
|
||||
packageId: String,
|
||||
module: String,
|
||||
template: String,
|
||||
arguments: Map[String, Any],
|
||||
): Command =
|
||||
Command().withCreate(
|
||||
CreateCommand(
|
||||
templateId = Some(buildIdentifier(packageId, module, template)),
|
||||
createArguments = Some(buildArguments(arguments)),
|
||||
)
|
||||
)
|
||||
|
||||
@Help.Summary("Build exercise command", FeatureFlag.Testing)
|
||||
def exercise(
|
||||
packageId: String,
|
||||
module: String,
|
||||
template: String,
|
||||
choice: String,
|
||||
arguments: Map[String, Any],
|
||||
contractId: String,
|
||||
): Command =
|
||||
Command().withExercise(
|
||||
ExerciseCommand(
|
||||
templateId = Some(buildIdentifier(packageId, module, template)),
|
||||
choice = choice,
|
||||
choiceArgument = Some(Value(Value.Sum.Record(buildArguments(arguments)))),
|
||||
contractId = contractId,
|
||||
)
|
||||
)
|
||||
|
||||
@Help.Summary("Build exercise command from CreatedEvent", FeatureFlag.Testing)
|
||||
def exercise(choice: String, arguments: Map[String, Any], event: CreatedEvent): Command = {
|
||||
def getOrThrow(desc: String, opt: Option[String]): String =
|
||||
opt.getOrElse(
|
||||
throw new IllegalArgumentException(s"Corrupt created event ${event} without ${desc}")
|
||||
)
|
||||
exercise(
|
||||
getOrThrow(
|
||||
"packageId",
|
||||
event.templateId
|
||||
.map(_.packageId),
|
||||
),
|
||||
getOrThrow("moduleName", event.templateId.map(_.moduleName)),
|
||||
getOrThrow("template", event.templateId.map(_.entityName)),
|
||||
choice,
|
||||
arguments,
|
||||
event.contractId,
|
||||
)
|
||||
}
|
||||
|
||||
// intentionally not publicly documented
|
||||
object jwt {
|
||||
def generate_unsafe_token_for_participant(
|
||||
participant: LocalParticipantReference,
|
||||
admin: Boolean,
|
||||
applicationId: String,
|
||||
): Map[PartyId, String] = {
|
||||
val secret = participant.config.ledgerApi.authServices
|
||||
.collectFirst { case AuthServiceConfig.UnsafeJwtHmac256(secret, _, _) =>
|
||||
secret.unwrap
|
||||
}
|
||||
.getOrElse("notasecret")
|
||||
|
||||
participant.parties
|
||||
.hosted()
|
||||
.map(_.party)
|
||||
.map(x =>
|
||||
(
|
||||
x,
|
||||
generate_unsafe_jwt256_token(
|
||||
secret = secret,
|
||||
admin = admin,
|
||||
readAs = List(x.toLf),
|
||||
actAs = List(x.toLf),
|
||||
ledgerId = Some(participant.id.uid.id.unwrap),
|
||||
applicationId = Some(applicationId),
|
||||
),
|
||||
)
|
||||
)
|
||||
.toMap
|
||||
}
|
||||
|
||||
def generate_unsafe_jwt256_token(
|
||||
secret: String,
|
||||
admin: Boolean,
|
||||
readAs: List[String],
|
||||
actAs: List[String],
|
||||
ledgerId: Option[String],
|
||||
applicationId: Option[String],
|
||||
): String = JwtTokenUtilities.buildUnsafeToken(
|
||||
secret = secret,
|
||||
admin = admin,
|
||||
readAs = readAs,
|
||||
actAs = actAs,
|
||||
ledgerId = ledgerId,
|
||||
applicationId = applicationId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Logging related commands")
|
||||
@Help.Group("Logging")
|
||||
object logging extends Helpful {
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Null"))
|
||||
@Help.Summary("Dynamically change log level (TRACE, DEBUG, INFO, WARN, ERROR, OFF, null)")
|
||||
def set_level(loggerName: String = "com.digitalasset.canton", level: String): Unit = {
|
||||
if (Seq("com.digitalasset.canton", "com.daml").exists(loggerName.startsWith))
|
||||
System.setProperty("LOG_LEVEL_CANTON", level)
|
||||
|
||||
val logger = getLogger(loggerName)
|
||||
if (level == "null")
|
||||
logger.setLevel(null)
|
||||
else
|
||||
logger.setLevel(Level.valueOf(level))
|
||||
}
|
||||
|
||||
@Help.Summary("Determine current logging level")
|
||||
def get_level(loggerName: String = "com.digitalasset.canton"): Option[Level] =
|
||||
Option(getLogger(loggerName).getLevel)
|
||||
|
||||
private def getLogger(loggerName: String): Logger = {
|
||||
import org.slf4j.LoggerFactory
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
val logger: Logger = LoggerFactory.getLogger(loggerName).asInstanceOf[Logger]
|
||||
logger
|
||||
}
|
||||
|
||||
private def getAppenders(logger: Logger): List[Appender[ILoggingEvent]] = {
|
||||
def go(currentAppender: Appender[ILoggingEvent]): List[Appender[ILoggingEvent]] = {
|
||||
currentAppender match {
|
||||
case attachable: AppenderAttachable[ILoggingEvent @unchecked] =>
|
||||
attachable.iteratorForAppenders().asScala.toList.flatMap(go)
|
||||
case appender: Appender[ILoggingEvent] => List(appender)
|
||||
}
|
||||
}
|
||||
|
||||
logger.iteratorForAppenders().asScala.toList.flatMap(go)
|
||||
}
|
||||
|
||||
private lazy val rootLogger = getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME)
|
||||
|
||||
private lazy val allAppenders = getAppenders(rootLogger)
|
||||
|
||||
private lazy val lastErrorsAppender: LastErrorsAppender = {
|
||||
findAppender("LAST_ERRORS") match {
|
||||
case Some(lastErrorsAppender: LastErrorsAppender) =>
|
||||
lastErrorsAppender
|
||||
case _ =>
|
||||
logger.error(s"Log appender for last errors not found/configured")
|
||||
throw new CommandFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private def findAppender(appenderName: String): Option[Appender[ILoggingEvent]] =
|
||||
Option(rootLogger.getAppender(appenderName))
|
||||
.orElse(allAppenders.find(_.getName == appenderName))
|
||||
|
||||
private def renderError(errorEvent: ILoggingEvent): String = {
|
||||
findAppender("FILE") match {
|
||||
case Some(appender: FileAppender[ILoggingEvent]) =>
|
||||
ByteString.copyFrom(appender.getEncoder.encode(errorEvent)).toStringUtf8
|
||||
case _ => errorEvent.getFormattedMessage
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Returns the last errors (trace-id -> error event) that have been logged locally")
|
||||
def last_errors(): Map[String, String] =
|
||||
lastErrorsAppender.lastErrors.fmap(renderError)
|
||||
|
||||
@Help.Summary("Returns log events for an error with the same trace-id")
|
||||
def last_error_trace(traceId: String): Seq[String] = {
|
||||
lastErrorsAppender.lastErrorTrace(traceId) match {
|
||||
case Some(events) => events.map(renderError)
|
||||
case None =>
|
||||
logger.error(s"No events found for last error trace-id $traceId")
|
||||
throw new CommandFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Configure behaviour of console")
|
||||
@Help.Group("Console")
|
||||
object console extends Helpful {
|
||||
|
||||
@Help.Summary("Yields the timeout for running console commands")
|
||||
@Help.Description(
|
||||
"Yields the timeout for running console commands. " +
|
||||
"When the timeout has elapsed, the console stops waiting for the command result. " +
|
||||
"The command will continue running in the background."
|
||||
)
|
||||
def command_timeout(implicit env: ConsoleEnvironment): NonNegativeDuration =
|
||||
env.commandTimeouts.bounded
|
||||
|
||||
@Help.Summary("Sets the timeout for running console commands.")
|
||||
@Help.Description(
|
||||
"Sets the timeout for running console commands. " +
|
||||
"When the timeout has elapsed, the console stops waiting for the command result. " +
|
||||
"The command will continue running in the background. " +
|
||||
"The new timeout must be positive."
|
||||
)
|
||||
def set_command_timeout(newTimeout: NonNegativeDuration)(implicit
|
||||
env: ConsoleEnvironment
|
||||
): Unit =
|
||||
env.setCommandTimeout(newTimeout)
|
||||
|
||||
// this command is intentionally not documented as part of the help system
|
||||
def disable_features(flag: FeatureFlag)(implicit env: ConsoleEnvironment): Unit = {
|
||||
env.updateFeatureSet(flag, include = false)
|
||||
}
|
||||
|
||||
// this command is intentionally not documented as part of the help system
|
||||
def enable_features(flag: FeatureFlag)(implicit env: ConsoleEnvironment): Unit = {
|
||||
env.updateFeatureSet(flag, include = true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ConsoleMacros extends ConsoleMacros with NamedLogging {
|
||||
val loggerFactory = NamedLoggerFactory.root
|
||||
}
|
||||
|
||||
object DebuggingHelpers extends LazyLogging {
|
||||
|
||||
def get_active_contracts(
|
||||
ref: LocalParticipantReference,
|
||||
limit: PositiveInt = PositiveInt.tryCreate(1000000),
|
||||
): (Map[String, String], Map[String, TemplateId]) =
|
||||
get_active_contracts_helper(
|
||||
ref,
|
||||
alias => ref.testing.pcs_search(alias, activeSet = true, limit = limit),
|
||||
)
|
||||
|
||||
def get_active_contracts_from_internal_db_state(
|
||||
ref: ParticipantReference,
|
||||
state: SyncStateInspection,
|
||||
limit: PositiveInt = PositiveInt.tryCreate(1000000),
|
||||
): (Map[String, String], Map[String, TemplateId]) =
|
||||
get_active_contracts_helper(
|
||||
ref,
|
||||
alias =>
|
||||
TraceContext.withNewTraceContext(implicit traceContext =>
|
||||
state.findContracts(alias, None, None, None, limit.value)
|
||||
),
|
||||
)
|
||||
|
||||
private def get_active_contracts_helper(
|
||||
ref: ParticipantReference,
|
||||
lookup: DomainAlias => Seq[(Boolean, SerializableContract)],
|
||||
): (Map[String, String], Map[String, TemplateId]) = {
|
||||
val syncAcs = ref.domains
|
||||
.list_connected()
|
||||
.map(_.domainAlias)
|
||||
.flatMap(lookup)
|
||||
.collect {
|
||||
case (active, sc) if active =>
|
||||
(sc.contractId.coid, sc.contractInstance.unversioned.template.qualifiedName.toString())
|
||||
}
|
||||
.toMap
|
||||
val lapiAcs = ref.ledger_api.acs.of_all().map(ev => (ev.event.contractId, ev.templateId)).toMap
|
||||
(syncAcs, lapiAcs)
|
||||
}
|
||||
|
||||
def diff_active_contracts(ref: LocalParticipantReference, limit: Int = 1000000): Unit = {
|
||||
val (syncAcs, lapiAcs) = get_active_contracts(ref, limit)
|
||||
if (syncAcs.sizeCompare(lapiAcs) != 0) {
|
||||
logger.error(s"Sync ACS differs ${syncAcs.size} from Ledger API ACS ${lapiAcs.size} in size")
|
||||
}
|
||||
|
||||
val lapiSet = lapiAcs.keySet
|
||||
val syncSet = syncAcs.keySet
|
||||
|
||||
def compare[V](
|
||||
explain: String,
|
||||
lft: Set[String],
|
||||
rght: Set[String],
|
||||
payload: Map[String, V],
|
||||
) = {
|
||||
val delta = lft.diff(rght)
|
||||
delta.foreach { key =>
|
||||
logger.info(s"${explain} ${key} ${payload.getOrElse(key, sys.error("should be there"))}")
|
||||
}
|
||||
}
|
||||
|
||||
compare("Active in LAPI but not in SYNC", lapiSet, syncSet, lapiAcs)
|
||||
compare("Active in SYNC but not in LAPI", syncSet, lapiSet, syncAcs)
|
||||
|
||||
}
|
||||
|
||||
def active_contracts_by_template(
|
||||
ref: LocalParticipantReference,
|
||||
limit: Int = 1000000,
|
||||
): (Map[String, Int], Map[TemplateId, Int]) = {
|
||||
val (syncAcs, lapiAcs) = get_active_contracts(ref, limit)
|
||||
val groupedSync = syncAcs.toSeq
|
||||
.map { x =>
|
||||
x.swap
|
||||
}
|
||||
.groupBy(_._1)
|
||||
.map(x => (x._1, x._2.length))
|
||||
val groupedLapi = lapiAcs.toSeq
|
||||
.map { x =>
|
||||
x.swap
|
||||
}
|
||||
.groupBy(_._1)
|
||||
.map(x => (x._1, x._2.length))
|
||||
(groupedSync, groupedLapi)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
/** Interface for output to the Console user.
|
||||
*/
|
||||
trait ConsoleOutput {
|
||||
|
||||
/** By default, commands should not output anything to the user. So use this only if it absolutely has to be.
|
||||
*
|
||||
* In particular:
|
||||
* - If there is an error, then report this to the log file. The log file will also be displayed to the user.
|
||||
* - If a command completes successfully, do not output anything.
|
||||
* - If a command returns some `value`, then make the command return the value (instead of printing the value to the console).
|
||||
* This allows the user to access the value programmatically.
|
||||
* (Make sure that `value.toString` creates a readable representation.)
|
||||
*/
|
||||
def info(message: String): Unit
|
||||
}
|
||||
|
||||
/** Logs directly to stdout and stderr.
|
||||
*/
|
||||
object StandardConsoleOutput extends ConsoleOutput {
|
||||
override def info(message: String): Unit = Console.out.println(message)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
sealed trait FeatureFlag {
|
||||
def configName: String
|
||||
}
|
||||
|
||||
object FeatureFlag {
|
||||
|
||||
object Stable extends FeatureFlag {
|
||||
val configName = "enabled-by-default"
|
||||
override def toString: String = "Stable"
|
||||
}
|
||||
|
||||
object Preview extends FeatureFlag {
|
||||
val configName = "enable-preview-commands"
|
||||
override def toString: String = "Preview"
|
||||
}
|
||||
|
||||
object Repair extends FeatureFlag {
|
||||
val configName = "enable-repair-commands"
|
||||
override def toString: String = "Repair"
|
||||
}
|
||||
|
||||
object Testing extends FeatureFlag {
|
||||
val configName = "enable-testing-commands"
|
||||
override def toString: String = "Testing"
|
||||
}
|
||||
|
||||
lazy val all = Set(Stable, Preview, Repair, Testing)
|
||||
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import better.files.File
|
||||
import com.digitalasset.canton.admin.api.client.commands.StatusAdminCommands
|
||||
import com.digitalasset.canton.admin.api.client.data.CantonStatus
|
||||
import com.digitalasset.canton.config.LocalNodeConfig
|
||||
import com.digitalasset.canton.console.CommandErrors.CommandError
|
||||
import com.digitalasset.canton.environment.Environment
|
||||
import com.digitalasset.canton.health.admin.data.NodeStatus
|
||||
import com.digitalasset.canton.health.admin.{data, v0}
|
||||
import com.digitalasset.canton.metrics.MetricsSnapshot
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.version.ReleaseVersion
|
||||
import io.circe.Encoder
|
||||
import io.circe.syntax.*
|
||||
|
||||
import scala.annotation.nowarn
|
||||
|
||||
/** Generates a health dump zip file containing information about the current Canton process
|
||||
* This is the core of the implementation of the HealthDump gRPC endpoint.
|
||||
*/
|
||||
trait HealthDumpGenerator[Status <: CantonStatus] {
|
||||
def status(): Status
|
||||
def environment: Environment
|
||||
def grpcAdminCommandRunner: GrpcAdminCommandRunner
|
||||
protected implicit val statusEncoder: Encoder[Status]
|
||||
|
||||
protected def getStatusForNode[S <: NodeStatus.Status](
|
||||
nodeName: String,
|
||||
nodeConfig: LocalNodeConfig,
|
||||
deserializer: v0.NodeStatus.Status => ParsingResult[S],
|
||||
): NodeStatus[S] = {
|
||||
grpcAdminCommandRunner
|
||||
.runCommand(
|
||||
nodeName,
|
||||
new StatusAdminCommands.GetStatus[S](deserializer),
|
||||
nodeConfig.clientAdminApi,
|
||||
None,
|
||||
) match {
|
||||
case CommandSuccessful(value) => value
|
||||
case err: CommandError => data.NodeStatus.Failure(err.cause)
|
||||
}
|
||||
}
|
||||
|
||||
protected def statusMap[S <: NodeStatus.Status](
|
||||
nodes: Map[String, LocalNodeConfig],
|
||||
deserializer: v0.NodeStatus.Status => ParsingResult[S],
|
||||
): Map[String, () => NodeStatus[S]] = {
|
||||
nodes.map { case (nodeName, nodeConfig) =>
|
||||
nodeName -> (() => getStatusForNode[S](nodeName, nodeConfig, deserializer))
|
||||
}
|
||||
}
|
||||
|
||||
@nowarn("cat=lint-byname-implicit") // https://github.com/scala/bug/issues/12072
|
||||
def generateHealthDump(
|
||||
outputFile: File,
|
||||
extraFilesToZip: Seq[File] = Seq.empty,
|
||||
): File = {
|
||||
import io.circe.generic.auto.*
|
||||
import CantonHealthAdministrationEncoders.*
|
||||
|
||||
final case class EnvironmentInfo(os: String, javaVersion: String)
|
||||
|
||||
final case class CantonDump(
|
||||
releaseVersion: String,
|
||||
environment: EnvironmentInfo,
|
||||
config: String,
|
||||
status: Status,
|
||||
metrics: MetricsSnapshot,
|
||||
traces: Map[Thread, Array[StackTraceElement]],
|
||||
)
|
||||
|
||||
val javaVersion = System.getProperty("java.version")
|
||||
val cantonVersion = ReleaseVersion.current.fullVersion
|
||||
val env = EnvironmentInfo(sys.props("os.name"), javaVersion)
|
||||
|
||||
val metricsSnapshot = MetricsSnapshot(
|
||||
environment.metricsFactory.registry,
|
||||
environment.configuredOpenTelemetry.onDemandMetricsReader,
|
||||
)
|
||||
val config = environment.config.dumpString
|
||||
|
||||
val traces = {
|
||||
import scala.jdk.CollectionConverters.*
|
||||
Thread.getAllStackTraces.asScala.toMap
|
||||
}
|
||||
|
||||
val dump = CantonDump(cantonVersion, env, config, status(), metricsSnapshot, traces)
|
||||
|
||||
val logFile =
|
||||
File(
|
||||
sys.env
|
||||
.get("LOG_FILE_NAME")
|
||||
.orElse(sys.props.get("LOG_FILE_NAME")) // This is set in Cli.installLogging
|
||||
.getOrElse("log/canton.log")
|
||||
)
|
||||
|
||||
val logLastErrorsFile = File(
|
||||
sys.env
|
||||
.get("LOG_LAST_ERRORS_FILE_NAME")
|
||||
.orElse(sys.props.get("LOG_LAST_ERRORS_FILE_NAME"))
|
||||
.getOrElse("log/canton_errors.log")
|
||||
)
|
||||
|
||||
// This is a guess based on the default logback config as to what the rolling log files look like
|
||||
// If we want to be more robust we'd have to access logback directly, extract the pattern from there, and use it to
|
||||
// glob files.
|
||||
val rollingLogs = logFile.siblings
|
||||
.filter { f =>
|
||||
f.name.contains(logFile.name) && f.extension.contains(".gz")
|
||||
}
|
||||
.toSeq
|
||||
.sortBy(_.name)
|
||||
.take(environment.config.monitoring.dumpNumRollingLogFiles.unwrap)
|
||||
|
||||
File.usingTemporaryFile("canton-dump-", ".json") { tmpFile =>
|
||||
tmpFile.append(dump.asJson.spaces2)
|
||||
val files = Iterator(logFile, logLastErrorsFile, tmpFile).filter(_.nonEmpty)
|
||||
outputFile.zipIn(files ++ extraFilesToZip.iterator ++ rollingLogs)
|
||||
}
|
||||
|
||||
outputFile
|
||||
}
|
||||
}
|
@ -0,0 +1,349 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import cats.syntax.functor.*
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
|
||||
import scala.annotation.StaticAnnotation
|
||||
import scala.reflect.ClassTag
|
||||
import scala.reflect.runtime.universe as ru
|
||||
|
||||
/** User friendly help messages generator.
|
||||
*/
|
||||
object Help {
|
||||
|
||||
private val defaultTopLevelTopicStr = "Top-level Commands"
|
||||
val defaultTopLevelTopic = Seq(defaultTopLevelTopicStr)
|
||||
|
||||
/** A short summary of the method (to be displayed in a list)
|
||||
*
|
||||
* Note that the annotation parser is also hard-coded to the default flag Stable
|
||||
*/
|
||||
final case class Summary(s: String, flag: FeatureFlag = FeatureFlag.Stable)
|
||||
extends StaticAnnotation {
|
||||
override def toString: String = s
|
||||
}
|
||||
|
||||
/** A longer description of the method */
|
||||
final case class Description(s: String) extends StaticAnnotation {
|
||||
override def toString: String = s
|
||||
}
|
||||
|
||||
/** Indicates that a command is only available for domain running at least the specified protocolVersion. */
|
||||
final case class AvailableFrom(protocolVersion: ProtocolVersion) extends StaticAnnotation {
|
||||
override def toString: String = protocolVersion.toString
|
||||
}
|
||||
|
||||
/** A sequence of strings classifying the method breadcrumb style (e.g. Seq("Participant", "Diagnostics")).
|
||||
* Used as headings in the displayed help.
|
||||
*/
|
||||
final case class Topic(t: Seq[String]) extends StaticAnnotation {
|
||||
override def toString(): String = t.mkString(": ")
|
||||
}
|
||||
|
||||
/** A tag to indicate nesting of items */
|
||||
final case class Group(name: String) extends StaticAnnotation {
|
||||
override def toString: String = name
|
||||
}
|
||||
|
||||
final case class MethodSignature(argsWithTypes: Seq[(String, String)], retType: String) {
|
||||
val argString = "(" + argsWithTypes.map(arg => s"${arg._1}: ${arg._2}").mkString(", ") + ")"
|
||||
val retString = s": $retType"
|
||||
override def toString(): String = argString + retString
|
||||
def noUnits(): String =
|
||||
(if (argsWithTypes.isEmpty) "" else argString) +
|
||||
(if (retType == "Unit") "" else retString)
|
||||
}
|
||||
|
||||
final case class Item(
|
||||
name: String,
|
||||
signature: Option[MethodSignature],
|
||||
summary: Summary,
|
||||
description: Description,
|
||||
topic: Topic,
|
||||
subItems: Seq[Item] = Seq.empty,
|
||||
)
|
||||
|
||||
/** Generate help messages from an object instance using reflection, using only the given summaries.
|
||||
*
|
||||
* ARGUMENTS OF THE ANNOTATIONS MUST BE LITERALS (CONSTANTS) (e.g., Topic(topicVariable) does not work).
|
||||
*
|
||||
* All methods with a [[Summary]] annotation will be included. [[Description]] or [[Topic]]
|
||||
* are also included if present, and are set to the empty string otherwise.
|
||||
* We attempt to make as friendly as possible:
|
||||
* - Unit types are not displayed
|
||||
* - Empty argument lists are dropped
|
||||
* - Implicits are hidden
|
||||
* See corresponding tests for examples.
|
||||
*/
|
||||
def forInstance[T: ClassTag](
|
||||
instance: T,
|
||||
baseTopic: Seq[String] = Seq(),
|
||||
scope: Set[FeatureFlag] = FeatureFlag.all,
|
||||
): String = {
|
||||
// type extractor
|
||||
val items = getItems(instance, baseTopic, scope)
|
||||
format(items: _*)
|
||||
}
|
||||
|
||||
def forMethod[T: ClassTag](
|
||||
instance: T,
|
||||
methodName: String,
|
||||
scope: Set[FeatureFlag] = FeatureFlag.all,
|
||||
): String =
|
||||
forMethod(getItems(instance, scope = scope), methodName)
|
||||
|
||||
def forMethod(items: Seq[Item], methodName: String): String = {
|
||||
def expand(item: Item): Seq[(String, Item)] = {
|
||||
(item.name, item) +: item.subItems.flatMap(expand).map { case (mn, itm) =>
|
||||
(item.name + "." + mn, itm)
|
||||
}
|
||||
}
|
||||
val expanded = items.flatMap(expand)
|
||||
val matching = expanded.filter { case (itemName, _) => itemName == methodName }
|
||||
if (matching.nonEmpty) {
|
||||
matching
|
||||
.map { case (itemName, item) =>
|
||||
formatItem(item.copy(name = itemName))
|
||||
}
|
||||
.mkString(System.lineSeparator())
|
||||
} else {
|
||||
val similarItems = expanded.map(_._1).filter(_.contains(methodName)).sorted.take(10)
|
||||
if (similarItems.isEmpty)
|
||||
s"Error: method $methodName not found; check your spelling"
|
||||
else {
|
||||
s"Error: method $methodName not found; are you looking for one of the following?\n ${similarItems
|
||||
.mkString("\n ")}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def formatItem(item: Item): String = item match {
|
||||
case Item(name, optSignature, summary, description, topic, group) =>
|
||||
val sigString = optSignature.map(_.noUnits()).getOrElse("")
|
||||
val text = if (description.s.nonEmpty) description.toString else summary.toString
|
||||
Seq(name + sigString, text).mkString(System.lineSeparator)
|
||||
}
|
||||
|
||||
private def extractItem(
|
||||
mirror: ru.Mirror,
|
||||
member: ru.Symbol,
|
||||
baseTopic: Seq[String],
|
||||
scope: Set[FeatureFlag],
|
||||
): Option[Item] =
|
||||
memberDescription(member)
|
||||
.filter { case (summary, _, _, _) => scope.contains(summary.flag) }
|
||||
.map { case (summary, description, topic, group) =>
|
||||
val methodName = member.name.toString
|
||||
val info = member.info
|
||||
val name = methodName
|
||||
val myTopic = Topic(baseTopic ++ topic.t ++ group.toList)
|
||||
val summaries = member.typeSignature.members
|
||||
.flatMap(s =>
|
||||
extractItem(mirror, s, myTopic.t, scope).toList
|
||||
) // filter to members that have `@Help.Summary` applied
|
||||
val signature = Some(methodSignature(info))
|
||||
val topicOrDefault =
|
||||
if (summaries.isEmpty && topic.t.isEmpty && group.isEmpty)
|
||||
Topic(baseTopic ++ defaultTopLevelTopic)
|
||||
else myTopic
|
||||
Item(name, signature, summary, description, topicOrDefault, summaries.toSeq)
|
||||
}
|
||||
|
||||
def getItems[T: ClassTag](
|
||||
instance: T,
|
||||
baseTopic: Seq[String] = Seq(),
|
||||
scope: Set[FeatureFlag] = FeatureFlag.all,
|
||||
): Seq[Item] = {
|
||||
val mirror = ru.runtimeMirror(getClass.getClassLoader)
|
||||
val mirroredType = mirror.reflect(instance)
|
||||
mirroredType.symbol.typeSignature.members
|
||||
.flatMap(m => extractItem(mirror, m, baseTopic, scope).toList)
|
||||
.toSeq
|
||||
}
|
||||
|
||||
def flattenItem(path: Seq[String])(item: Item): Seq[Item] = {
|
||||
val newPath = path :+ item.name
|
||||
item.copy(name = newPath.mkString(".")) +: item.subItems.flatMap(flattenItem(newPath))
|
||||
}
|
||||
|
||||
def flattenItemsForManual(items: Seq[Item]): Seq[Item] =
|
||||
items
|
||||
.flatMap(flattenItem(Seq()))
|
||||
.filter(_.subItems.isEmpty)
|
||||
// strip trailing default topic such that we don't have to add "Top-level Commands" to every group in the manual
|
||||
.map { itm =>
|
||||
itm.copy(
|
||||
topic =
|
||||
if (
|
||||
itm.topic.t.lengthCompare(1) > 0 && itm.topic.t.lastOption
|
||||
.contains(defaultTopLevelTopicStr)
|
||||
)
|
||||
Topic(itm.topic.t.take(itm.topic.t.length - 1))
|
||||
else itm.topic
|
||||
)
|
||||
}
|
||||
|
||||
def getItemsFlattenedForManual[T: ClassTag](
|
||||
instance: T,
|
||||
baseTopic: Seq[String] = Seq(),
|
||||
): Seq[Item] =
|
||||
flattenItemsForManual(getItems(instance, baseTopic))
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
private def methodSignature[T](typ: ru.Type): MethodSignature = {
|
||||
val methodType = typ.asInstanceOf[ru.TypeApi]
|
||||
|
||||
def excludeImplicits(symbols: List[ru.Symbol]): List[ru.Symbol] =
|
||||
symbols.filter(!_.isImplicit)
|
||||
|
||||
def until[U](p: U => Boolean, f: U => U)(x: U): U =
|
||||
if (p(x)) x else until(p, f)(f(x))
|
||||
|
||||
val args =
|
||||
excludeImplicits(methodType.paramLists.flatten).map(symb =>
|
||||
(symb.name.toString, symb.typeSignature.toString)
|
||||
)
|
||||
// return types can contain implicit parameter lists; ensure that these are excluded
|
||||
val returnType =
|
||||
until[ru.Type](typ => !typ.paramLists.flatten.exists(_.isImplicit), _.resultType)(
|
||||
methodType.resultType
|
||||
)
|
||||
MethodSignature(args, returnType.toString)
|
||||
}
|
||||
|
||||
def fromObject[T: ClassTag](instance: T): Seq[Item] =
|
||||
getItems(instance)
|
||||
|
||||
/** Format help for named items and their descriptions
|
||||
* @param items Tuple of name and description
|
||||
*/
|
||||
def format(items: Item*): String = {
|
||||
def underline(s: String) = s + System.lineSeparator + "-" * s.length + System.lineSeparator()
|
||||
val grouped = items
|
||||
.filter(_.subItems.nonEmpty)
|
||||
.sortBy(_.name)
|
||||
.map { case Item(name, signature, Summary(summary, flag), description, topic, _) =>
|
||||
s"$name - $summary"
|
||||
}
|
||||
.toList
|
||||
|
||||
val topLevel = items
|
||||
.filter(_.subItems.isEmpty)
|
||||
.groupBy(_.topic)
|
||||
.fmap(descs =>
|
||||
descs
|
||||
.sortBy(_.name) // sort alphabetically
|
||||
.map { case Item(name, signature, Summary(summary, flag), description, topic, _) =>
|
||||
s"$name - $summary"
|
||||
}
|
||||
.mkString(System.lineSeparator)
|
||||
)
|
||||
.toList
|
||||
.sortWith { (x, y) =>
|
||||
(x._1.t, y._1.t) match {
|
||||
case (`defaultTopLevelTopic`, _) => true
|
||||
case (_, `defaultTopLevelTopic`) => false
|
||||
case (lft, rght) => lft.mkString(".") < rght.mkString(".")
|
||||
}
|
||||
}
|
||||
.map({ case (tobj @ Topic(topic), h) =>
|
||||
(if (topic.nonEmpty) underline(tobj.toString) else "") + h
|
||||
})
|
||||
.mkString(System.lineSeparator + System.lineSeparator)
|
||||
if (grouped.nonEmpty) {
|
||||
topLevel + System.lineSeparator + System.lineSeparator + underline("Command Groups") + grouped
|
||||
.mkString(System.lineSeparator)
|
||||
} else topLevel
|
||||
}
|
||||
|
||||
private def memberDescription(
|
||||
member: ru.Symbol
|
||||
): Option[(Summary, Description, Topic, Option[String])] = {
|
||||
(
|
||||
member.annotations.map(fromAnnotation(_, summaryParser)).collect { case Some(s) => s },
|
||||
member.annotations.map(fromAnnotation(_, descriptionParser)).collect { case Some(s) => s },
|
||||
member.annotations.map(fromAnnotation(_, tagParser)).collect { case Some(s) => s },
|
||||
member.annotations.map(fromAnnotation(_, groupParser)).collect { case Some(s) => s },
|
||||
) match {
|
||||
case (Nil, _, _, _) => None
|
||||
case (summary :: _sums, l2, l3, g4) =>
|
||||
Some(
|
||||
(
|
||||
summary,
|
||||
l2.headOption.getOrElse(Description("")),
|
||||
l3.headOption.getOrElse(Topic(Seq())),
|
||||
g4.headOption.map(_.name),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The following definitions (fromAnnotation and Xparser) are quite nasty.
|
||||
* They hackily reconstruct the Scala values from annotations, which contain ASTs.
|
||||
* It's possible to use a reflection Toolbox to eval the annotations, but this is very slow (as it entails compiling the ASTs
|
||||
* from the annotations) and results in large latencies for the help command.
|
||||
*/
|
||||
private def fromAnnotation[T: ru.TypeTag](
|
||||
annotation: ru.Annotation,
|
||||
parser: ru.Tree => T,
|
||||
): Option[T] = {
|
||||
if (annotation.tree.tpe.typeSymbol == ru.typeOf[T].typeSymbol) {
|
||||
Some(parser(annotation.tree))
|
||||
} else None
|
||||
}
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
private def grabStringTag(tree: ru.Tree): String =
|
||||
tree
|
||||
.children(1)
|
||||
.asInstanceOf[ru.Literal]
|
||||
.value
|
||||
.value
|
||||
.asInstanceOf[String]
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
private def summaryParser(tree: ru.Tree): Summary = {
|
||||
def grabFeatureFlagFromSummary(tree: ru.Tree): FeatureFlag =
|
||||
if (tree.children.lengthCompare(2) > 0) {
|
||||
val tmp = tree.children(2).asInstanceOf[ru.Select]
|
||||
if (tmp.symbol.isModule) {
|
||||
reflect.runtime.currentMirror
|
||||
.reflectModule(tmp.symbol.asModule)
|
||||
.instance
|
||||
.asInstanceOf[FeatureFlag]
|
||||
} else FeatureFlag.Stable
|
||||
} else FeatureFlag.Stable
|
||||
Summary(grabStringTag(tree), grabFeatureFlagFromSummary(tree))
|
||||
}
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
private def descriptionParser(tree: ru.Tree): Description = {
|
||||
try {
|
||||
Description(grabStringTag(tree).stripMargin)
|
||||
} catch {
|
||||
case x: RuntimeException =>
|
||||
// leave a comment for the poor developer that might run into the same issue ...
|
||||
println(
|
||||
"Failed to process description (description needs to be a string. i.e. don't apply stripmargin here ...): " + tree.toString
|
||||
)
|
||||
throw x
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
private def tagParser(tree: ru.Tree): Topic = {
|
||||
val args = tree
|
||||
.children(1)
|
||||
.children
|
||||
.drop(1)
|
||||
.map(l => l.asInstanceOf[ru.Literal].value.value.asInstanceOf[String])
|
||||
Topic(args)
|
||||
}
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
private def groupParser(tree: ru.Tree): Group = Group(grabStringTag(tree))
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
/** Implementors will have a `help` method available that will be callable from the Console.
|
||||
* Implementors should annotate appropriate methods with `@Help.Summary` to have them included.
|
||||
*/
|
||||
trait Helpful {
|
||||
|
||||
def help()(implicit consoleEnvironment: ConsoleEnvironment): Unit = {
|
||||
val featureSet = consoleEnvironment.featureSet
|
||||
consoleEnvironment.consoleOutput.info(Help.forInstance(this, scope = featureSet))
|
||||
}
|
||||
|
||||
@Help.Summary("Help for specific commands (use help() or help(\"method\") for more information)")
|
||||
@Help.Topic(Seq("Top-level Commands"))
|
||||
def help(methodName: String)(implicit consoleEnvironment: ConsoleEnvironment): Unit =
|
||||
consoleEnvironment.consoleOutput.info(
|
||||
Help.forMethod(this, methodName, scope = consoleEnvironment.featureSet)
|
||||
)
|
||||
|
||||
}
|
@ -0,0 +1,810 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import com.daml.lf.data.Ref.PackageId
|
||||
import com.digitalasset.canton.*
|
||||
import com.digitalasset.canton.admin.api.client.commands.GrpcAdminCommand
|
||||
import com.digitalasset.canton.config.RequireTypes.Port
|
||||
import com.digitalasset.canton.config.*
|
||||
import com.digitalasset.canton.console.CommandErrors.NodeNotStarted
|
||||
import com.digitalasset.canton.console.commands.*
|
||||
import com.digitalasset.canton.crypto.Crypto
|
||||
import com.digitalasset.canton.domain.config.RemoteDomainConfig
|
||||
import com.digitalasset.canton.domain.{Domain, DomainNodeBootstrap}
|
||||
import com.digitalasset.canton.environment.*
|
||||
import com.digitalasset.canton.health.admin.data.{DomainStatus, NodeStatus, ParticipantStatus}
|
||||
import com.digitalasset.canton.logging.pretty.{Pretty, PrettyPrinting}
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger}
|
||||
import com.digitalasset.canton.participant.config.{
|
||||
BaseParticipantConfig,
|
||||
LocalParticipantConfig,
|
||||
RemoteParticipantConfig,
|
||||
}
|
||||
import com.digitalasset.canton.participant.domain.DomainConnectionConfig
|
||||
import com.digitalasset.canton.participant.{
|
||||
ParticipantNode,
|
||||
ParticipantNodeBootstrapX,
|
||||
ParticipantNodeCommon,
|
||||
ParticipantNodeX,
|
||||
}
|
||||
import com.digitalasset.canton.sequencing.{SequencerConnection, SequencerConnections}
|
||||
import com.digitalasset.canton.topology.{DomainId, NodeIdentity, ParticipantId}
|
||||
import com.digitalasset.canton.tracing.NoTracing
|
||||
import com.digitalasset.canton.util.ErrorUtil
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.hashing.MurmurHash3
|
||||
|
||||
trait InstanceReferenceCommon
|
||||
extends AdminCommandRunner
|
||||
with Helpful
|
||||
with NamedLogging
|
||||
with FeatureFlagFilter
|
||||
with PrettyPrinting {
|
||||
|
||||
val name: String
|
||||
protected val instanceType: String
|
||||
|
||||
protected[canton] def executionContext: ExecutionContext
|
||||
|
||||
override def pretty: Pretty[InstanceReferenceCommon] =
|
||||
prettyOfString(inst => show"${inst.instanceType.unquoted} ${inst.name.singleQuoted}")
|
||||
|
||||
val consoleEnvironment: ConsoleEnvironment
|
||||
|
||||
override protected[console] def tracedLogger: TracedLogger = logger
|
||||
|
||||
override def hashCode(): Int = {
|
||||
val init = this.getClass.hashCode()
|
||||
val t1 = MurmurHash3.mix(init, consoleEnvironment.hashCode())
|
||||
val t2 = MurmurHash3.mix(t1, name.hashCode)
|
||||
t2
|
||||
}
|
||||
|
||||
// this is just testing, because the cached values should remain unchanged in operation
|
||||
@Help.Summary("Clear locally cached variables", FeatureFlag.Testing)
|
||||
@Help.Description(
|
||||
"Some commands cache values on the client side. Use this command to explicitly clear the caches of these values."
|
||||
)
|
||||
def clear_cache(): Unit = {
|
||||
topology.clearCache()
|
||||
}
|
||||
|
||||
type Status <: NodeStatus.Status
|
||||
|
||||
def id: NodeIdentity
|
||||
|
||||
def health: HealthAdministrationCommon[Status]
|
||||
|
||||
def keys: KeyAdministrationGroup
|
||||
|
||||
def topology: TopologyAdministrationGroupCommon
|
||||
}
|
||||
|
||||
/** Reference to "Old" daml 2.x nodes have:
|
||||
* - parties admin commands
|
||||
* - "old" topology admin commands based on "old" TopologyChangeOp
|
||||
*/
|
||||
trait InstanceReference extends InstanceReferenceCommon {
|
||||
def parties: PartiesAdministrationGroup
|
||||
override def topology: TopologyAdministrationGroup
|
||||
}
|
||||
|
||||
/** InstanceReferenceX with different topology administration x
|
||||
*/
|
||||
trait InstanceReferenceX extends InstanceReferenceCommon {
|
||||
override def topology: TopologyAdministrationGroupX
|
||||
|
||||
private lazy val trafficControl_ =
|
||||
new TrafficControlAdministrationGroup(
|
||||
this,
|
||||
topology,
|
||||
this,
|
||||
consoleEnvironment,
|
||||
loggerFactory,
|
||||
)
|
||||
@Help.Summary("Traffic control related commands")
|
||||
@Help.Group("Traffic")
|
||||
def traffic_control: TrafficControlAdministrationGroup = trafficControl_
|
||||
}
|
||||
|
||||
/** Pointer for a potentially running instance by instance type (domain/participant) and its id.
|
||||
* These methods define the REPL interface for these instances (e.g. participant1 start)
|
||||
*/
|
||||
trait LocalInstanceReferenceCommon extends InstanceReferenceCommon with NoTracing {
|
||||
|
||||
val name: String
|
||||
val consoleEnvironment: ConsoleEnvironment
|
||||
private[console] val nodes: Nodes[CantonNode, CantonNodeBootstrap[CantonNode]]
|
||||
|
||||
@Help.Summary("Database related operations")
|
||||
@Help.Group("Database")
|
||||
object db extends Helpful {
|
||||
|
||||
@Help.Summary("Migrates the instance's database if using a database storage")
|
||||
def migrate(): Unit = consoleEnvironment.run(migrateDbCommand())
|
||||
|
||||
@Help.Summary(
|
||||
"Only use when advised - repairs the database migration of the instance's database"
|
||||
)
|
||||
@Help.Description(
|
||||
"""In some rare cases, we change already applied database migration files in a new release and the repair
|
||||
|command resets the checksums we use to ensure that in general already applied migration files have not been changed.
|
||||
|You should only use `db.repair_migration` when advised and otherwise use it at your own risk - in the worst case running
|
||||
|it may lead to data corruption when an incompatible database migration (one that should be rejected because
|
||||
|the already applied database migration files have changed) is subsequently falsely applied.
|
||||
|"""
|
||||
)
|
||||
def repair_migration(force: Boolean = false): Unit =
|
||||
consoleEnvironment.run(repairMigrationCommand(force))
|
||||
|
||||
}
|
||||
|
||||
@Help.Summary("Start the instance")
|
||||
def start(): Unit = consoleEnvironment.run(startCommand())
|
||||
|
||||
@Help.Summary("Stop the instance")
|
||||
def stop(): Unit = consoleEnvironment.run(stopCommand())
|
||||
|
||||
@Help.Summary("Check if the local instance is running")
|
||||
def is_running: Boolean = nodes.isRunning(name)
|
||||
|
||||
@Help.Summary("Check if the local instance is running and is fully initialized")
|
||||
def is_initialized: Boolean = nodes.getRunning(name).exists(_.isInitialized)
|
||||
|
||||
@Help.Summary("Config of node instance")
|
||||
def config: LocalNodeConfig
|
||||
|
||||
@Help.Summary("Manage public and secret keys")
|
||||
@Help.Group("Keys")
|
||||
override def keys: LocalKeyAdministrationGroup = _keys
|
||||
|
||||
private val _keys =
|
||||
new LocalKeyAdministrationGroup(this, this, consoleEnvironment, crypto, loggerFactory)(
|
||||
executionContext
|
||||
)
|
||||
|
||||
private[console] def migrateDbCommand(): ConsoleCommandResult[Unit] =
|
||||
migrateInstanceDb().toResult(_.message, _ => ())
|
||||
|
||||
private[console] def repairMigrationCommand(force: Boolean): ConsoleCommandResult[Unit] =
|
||||
repairMigrationOfInstance(force).toResult(_.message, _ => ())
|
||||
|
||||
private[console] def startCommand(): ConsoleCommandResult[Unit] =
|
||||
startInstance()
|
||||
.toResult({
|
||||
case m: PendingDatabaseMigration =>
|
||||
s"${m.message} Please run `${m.name}.db.migrate` to apply pending migrations"
|
||||
case m => m.message
|
||||
})
|
||||
|
||||
private[console] def stopCommand(): ConsoleCommandResult[Unit] =
|
||||
try {
|
||||
stopInstance().toResult(_.message)
|
||||
} finally {
|
||||
ErrorUtil.withThrowableLogging(clear_cache())
|
||||
}
|
||||
|
||||
protected def migrateInstanceDb(): Either[StartupError, _] = nodes.migrateDatabase(name)
|
||||
protected def repairMigrationOfInstance(force: Boolean): Either[StartupError, Unit] = {
|
||||
Either
|
||||
.cond(force, (), DidntUseForceOnRepairMigration(name))
|
||||
.flatMap(_ => nodes.repairDatabaseMigration(name))
|
||||
}
|
||||
|
||||
protected def startInstance(): Either[StartupError, Unit] =
|
||||
nodes.startAndWait(name)
|
||||
protected def stopInstance(): Either[ShutdownError, Unit] = nodes.stopAndWait(name)
|
||||
protected[canton] def crypto: Crypto
|
||||
|
||||
protected def runCommandIfRunning[Result](
|
||||
runner: => ConsoleCommandResult[Result]
|
||||
): ConsoleCommandResult[Result] =
|
||||
if (is_running)
|
||||
runner
|
||||
else
|
||||
NodeNotStarted.ErrorCanton(this)
|
||||
|
||||
override protected[console] def adminCommand[Result](
|
||||
grpcCommand: GrpcAdminCommand[_, _, Result]
|
||||
): ConsoleCommandResult[Result] = {
|
||||
runCommandIfRunning(
|
||||
consoleEnvironment.grpcAdminCommandRunner
|
||||
.runCommand(name, grpcCommand, config.clientAdminApi, None)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait LocalInstanceReference extends LocalInstanceReferenceCommon with InstanceReference
|
||||
trait LocalInstanceReferenceX extends LocalInstanceReferenceCommon with InstanceReferenceX
|
||||
|
||||
trait RemoteInstanceReference extends InstanceReferenceCommon {
|
||||
@Help.Summary("Manage public and secret keys")
|
||||
@Help.Group("Keys")
|
||||
override val keys: KeyAdministrationGroup =
|
||||
new KeyAdministrationGroup(this, this, consoleEnvironment, loggerFactory)
|
||||
}
|
||||
|
||||
trait GrpcRemoteInstanceReference extends RemoteInstanceReference {
|
||||
|
||||
def config: NodeConfig
|
||||
|
||||
override protected[console] def adminCommand[Result](
|
||||
grpcCommand: GrpcAdminCommand[_, _, Result]
|
||||
): ConsoleCommandResult[Result] =
|
||||
consoleEnvironment.grpcAdminCommandRunner.runCommand(
|
||||
name,
|
||||
grpcCommand,
|
||||
config.clientAdminApi,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
object DomainReference {
|
||||
val InstanceType = "Domain"
|
||||
}
|
||||
|
||||
trait DomainReference
|
||||
extends InstanceReference
|
||||
with DomainAdministration
|
||||
with InstanceReferenceWithSequencer {
|
||||
val consoleEnvironment: ConsoleEnvironment
|
||||
val name: String
|
||||
|
||||
override protected val instanceType: String = DomainReference.InstanceType
|
||||
|
||||
override type Status = DomainStatus
|
||||
|
||||
@Help.Summary("Health and diagnostic related commands")
|
||||
@Help.Group("Health")
|
||||
override def health =
|
||||
new HealthAdministration[DomainStatus](
|
||||
this,
|
||||
consoleEnvironment,
|
||||
DomainStatus.fromProtoV0,
|
||||
)
|
||||
|
||||
@Help.Summary(
|
||||
"Yields the globally unique id of this domain. " +
|
||||
"Throws an exception, if the id has not yet been allocated (e.g., the domain has not yet been started)."
|
||||
)
|
||||
def id: DomainId = topology.idHelper(DomainId(_))
|
||||
|
||||
private lazy val topology_ =
|
||||
new TopologyAdministrationGroup(
|
||||
this,
|
||||
this.health.status.successOption.map(_.topologyQueue),
|
||||
consoleEnvironment,
|
||||
loggerFactory,
|
||||
)
|
||||
@Help.Summary("Topology management related commands")
|
||||
@Help.Group("Topology")
|
||||
@Help.Description("This group contains access to the full set of topology management commands.")
|
||||
override def topology: TopologyAdministrationGroup = topology_
|
||||
|
||||
override protected val loggerFactory: NamedLoggerFactory = NamedLoggerFactory("domain", name)
|
||||
|
||||
override def equals(obj: Any): Boolean = {
|
||||
obj match {
|
||||
case x: DomainReference => x.consoleEnvironment == consoleEnvironment && x.name == name
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Inspect configured parties")
|
||||
@Help.Group("Parties")
|
||||
override def parties: PartiesAdministrationGroup = partiesGroup
|
||||
|
||||
// above command needs to be def such that `Help` works.
|
||||
lazy private val partiesGroup = new PartiesAdministrationGroup(this, consoleEnvironment)
|
||||
|
||||
private lazy val sequencer_ =
|
||||
new SequencerAdministrationGroup(this, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Manage the sequencer")
|
||||
@Help.Group("Sequencer")
|
||||
override def sequencer: SequencerAdministrationGroup = sequencer_
|
||||
|
||||
private lazy val mediator_ =
|
||||
new MediatorAdministrationGroup(this, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Manage the mediator")
|
||||
@Help.Group("Mediator")
|
||||
def mediator: MediatorAdministrationGroup = mediator_
|
||||
|
||||
@Help.Summary(
|
||||
"Yields a domain connection config with default values except for the domain alias and the sequencer connection. " +
|
||||
"May throw an exception if the domain alias or sequencer connection is misconfigured."
|
||||
)
|
||||
def defaultDomainConnection: DomainConnectionConfig =
|
||||
DomainConnectionConfig(
|
||||
DomainAlias.tryCreate(name),
|
||||
SequencerConnections.single(sequencerConnection),
|
||||
)
|
||||
}
|
||||
|
||||
trait RemoteDomainReference extends DomainReference with GrpcRemoteInstanceReference {
|
||||
val consoleEnvironment: ConsoleEnvironment
|
||||
val name: String
|
||||
|
||||
@Help.Summary("Returns the remote domain configuration")
|
||||
def config: RemoteDomainConfig =
|
||||
consoleEnvironment.environment.config.remoteDomainsByString(name)
|
||||
|
||||
override def sequencerConnection: SequencerConnection =
|
||||
config.publicApi.toConnection
|
||||
.fold(
|
||||
err => sys.error(s"Domain $name has invalid sequencer connection config: $err"),
|
||||
identity,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
trait CommunityDomainReference {
|
||||
this: DomainReference =>
|
||||
}
|
||||
|
||||
class CommunityRemoteDomainReference(val consoleEnvironment: ConsoleEnvironment, val name: String)
|
||||
extends DomainReference
|
||||
with CommunityDomainReference
|
||||
with RemoteDomainReference {
|
||||
|
||||
override protected[canton] def executionContext: ExecutionContext =
|
||||
consoleEnvironment.environment.executionContext
|
||||
}
|
||||
|
||||
trait InstanceReferenceWithSequencerConnection extends InstanceReferenceCommon {
|
||||
def sequencerConnection: SequencerConnection
|
||||
}
|
||||
trait InstanceReferenceWithSequencer extends InstanceReferenceWithSequencerConnection {
|
||||
def sequencer: SequencerAdministrationGroup
|
||||
}
|
||||
|
||||
trait LocalDomainReference
|
||||
extends DomainReference
|
||||
with BaseInspection[Domain]
|
||||
with LocalInstanceReference {
|
||||
override private[console] val nodes = consoleEnvironment.environment.domains
|
||||
|
||||
@Help.Summary("Returns the domain configuration")
|
||||
def config: consoleEnvironment.environment.config.DomainConfigType =
|
||||
consoleEnvironment.environment.config.domainsByString(name)
|
||||
|
||||
override def sequencerConnection: SequencerConnection =
|
||||
config.sequencerConnectionConfig.toConnection
|
||||
.fold(
|
||||
err => sys.error(s"Domain $name has invalid sequencer connection config: $err"),
|
||||
identity,
|
||||
)
|
||||
|
||||
override protected[console] def runningNode: Option[DomainNodeBootstrap] =
|
||||
consoleEnvironment.environment.domains.getRunning(name)
|
||||
|
||||
override protected[console] def startingNode: Option[DomainNodeBootstrap] =
|
||||
consoleEnvironment.environment.domains.getStarting(name)
|
||||
}
|
||||
|
||||
class CommunityLocalDomainReference(
|
||||
override val consoleEnvironment: ConsoleEnvironment,
|
||||
val name: String,
|
||||
override protected[canton] val executionContext: ExecutionContext,
|
||||
) extends DomainReference
|
||||
with CommunityDomainReference
|
||||
with LocalDomainReference
|
||||
|
||||
/** Bare, Canton agnostic parts of the ledger-api client
|
||||
*
|
||||
* This implementation allows to access any kind of ledger-api client, which does not need to be Canton based.
|
||||
* However, this comes at some cost, as some of the synchronization between nodes during transaction submission
|
||||
* is not supported
|
||||
*
|
||||
* @param hostname the hostname of the ledger api server
|
||||
* @param port the port of the ledger api server
|
||||
* @param tls the tls config to use on the client
|
||||
* @param token the jwt token to use on the client
|
||||
*/
|
||||
class ExternalLedgerApiClient(
|
||||
hostname: String,
|
||||
port: Port,
|
||||
tls: Option[TlsClientConfig],
|
||||
val token: Option[String] = None,
|
||||
)(implicit val consoleEnvironment: ConsoleEnvironment)
|
||||
extends BaseLedgerApiAdministration
|
||||
with LedgerApiCommandRunner
|
||||
with FeatureFlagFilter
|
||||
with NamedLogging {
|
||||
|
||||
override protected val name: String = s"$hostname:${port.unwrap}"
|
||||
|
||||
override val loggerFactory: NamedLoggerFactory =
|
||||
consoleEnvironment.environment.loggerFactory.append("client", name)
|
||||
|
||||
override protected def domainOfTransaction(transactionId: String): DomainId =
|
||||
throw new NotImplementedError("domain_of is not implemented for external ledger api clients")
|
||||
|
||||
override protected[console] def ledgerApiCommand[Result](
|
||||
command: GrpcAdminCommand[_, _, Result]
|
||||
): ConsoleCommandResult[Result] =
|
||||
consoleEnvironment.grpcAdminCommandRunner
|
||||
.runCommand("sourceLedger", command, ClientConfig(hostname, port, tls), token)
|
||||
|
||||
override protected def optionallyAwait[Tx](
|
||||
tx: Tx,
|
||||
txId: String,
|
||||
optTimeout: Option[NonNegativeDuration],
|
||||
): Tx = tx
|
||||
|
||||
}
|
||||
|
||||
object ExternalLedgerApiClient {
|
||||
|
||||
def forReference(participant: LocalParticipantReference, token: String)(implicit
|
||||
env: ConsoleEnvironment
|
||||
): ExternalLedgerApiClient = {
|
||||
val cc = participant.config.ledgerApi.clientConfig
|
||||
new ExternalLedgerApiClient(
|
||||
cc.address,
|
||||
cc.port,
|
||||
cc.tls,
|
||||
Some(token),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object ParticipantReference {
|
||||
val InstanceType = "Participant"
|
||||
}
|
||||
|
||||
sealed trait ParticipantReferenceCommon
|
||||
extends ConsoleCommandGroup
|
||||
with ParticipantAdministration
|
||||
with LedgerApiAdministration
|
||||
with LedgerApiCommandRunner
|
||||
with AdminCommandRunner
|
||||
with InstanceReferenceCommon {
|
||||
|
||||
override type Status = ParticipantStatus
|
||||
|
||||
override protected val loggerFactory: NamedLoggerFactory =
|
||||
consoleEnvironment.environment.loggerFactory.append("participant", name)
|
||||
|
||||
@Help.Summary(
|
||||
"Yields the globally unique id of this participant. " +
|
||||
"Throws an exception, if the id has not yet been allocated (e.g., the participant has not yet been started)."
|
||||
)
|
||||
override def id: ParticipantId = topology.idHelper(ParticipantId(_))
|
||||
|
||||
def config: BaseParticipantConfig
|
||||
|
||||
@Help.Summary("Commands used for development and testing", FeatureFlag.Testing)
|
||||
@Help.Group("Testing")
|
||||
def testing: ParticipantTestingGroup
|
||||
|
||||
@Help.Summary("Commands to pruning the archive of the ledger", FeatureFlag.Preview)
|
||||
@Help.Group("Ledger Pruning")
|
||||
def pruning: ParticipantPruningAdministrationGroup = pruning_
|
||||
private lazy val pruning_ =
|
||||
new ParticipantPruningAdministrationGroup(this, consoleEnvironment, loggerFactory)
|
||||
|
||||
@Help.Summary("Manage participant replication")
|
||||
@Help.Group("Replication")
|
||||
def replication: ParticipantReplicationAdministrationGroup = replicationGroup
|
||||
lazy private val replicationGroup =
|
||||
new ParticipantReplicationAdministrationGroup(this, consoleEnvironment)
|
||||
|
||||
@Help.Summary("Commands to repair the participant contract state", FeatureFlag.Repair)
|
||||
@Help.Group("Repair")
|
||||
def repair: ParticipantRepairAdministration
|
||||
|
||||
override def health
|
||||
: HealthAdministrationCommon[ParticipantStatus] & ParticipantHealthAdministrationCommon
|
||||
|
||||
}
|
||||
|
||||
abstract class ParticipantReference(
|
||||
override val consoleEnvironment: ConsoleEnvironment,
|
||||
val name: String,
|
||||
) extends ParticipantReferenceCommon
|
||||
with InstanceReference {
|
||||
|
||||
protected def runner: AdminCommandRunner = this
|
||||
|
||||
override protected val instanceType: String = ParticipantReference.InstanceType
|
||||
|
||||
@Help.Summary("Health and diagnostic related commands")
|
||||
@Help.Group("Health")
|
||||
override def health: ParticipantHealthAdministration =
|
||||
new ParticipantHealthAdministration(this, consoleEnvironment, loggerFactory)
|
||||
|
||||
@Help.Summary("Inspect and manage parties")
|
||||
@Help.Group("Parties")
|
||||
def parties: ParticipantPartiesAdministrationGroup
|
||||
|
||||
@Help.Summary(
|
||||
"Yields the globally unique id of this participant. " +
|
||||
"Throws an exception, if the id has not yet been allocated (e.g., the participant has not yet been started)."
|
||||
)
|
||||
override def id: ParticipantId = topology.idHelper(ParticipantId(_))
|
||||
|
||||
private lazy val topology_ =
|
||||
new TopologyAdministrationGroup(
|
||||
this,
|
||||
health.status.successOption.map(_.topologyQueue),
|
||||
consoleEnvironment,
|
||||
loggerFactory,
|
||||
)
|
||||
@Help.Summary("Topology management related commands")
|
||||
@Help.Group("Topology")
|
||||
@Help.Description("This group contains access to the full set of topology management commands.")
|
||||
def topology: TopologyAdministrationGroup = topology_
|
||||
override protected def vettedPackagesOfParticipant(): Set[PackageId] = topology.vetted_packages
|
||||
.list(filterStore = "Authorized", filterParticipant = id.filterString)
|
||||
.flatMap(_.item.packageIds)
|
||||
.toSet
|
||||
|
||||
override protected def participantIsActiveOnDomain(
|
||||
domainId: DomainId,
|
||||
participantId: ParticipantId,
|
||||
): Boolean = topology.participant_domain_states.active(domainId, participantId)
|
||||
}
|
||||
|
||||
sealed trait RemoteParticipantReferenceCommon
|
||||
extends LedgerApiCommandRunner
|
||||
with ParticipantReferenceCommon {
|
||||
|
||||
def config: RemoteParticipantConfig
|
||||
|
||||
override protected[console] def ledgerApiCommand[Result](
|
||||
command: GrpcAdminCommand[_, _, Result]
|
||||
): ConsoleCommandResult[Result] =
|
||||
consoleEnvironment.grpcAdminCommandRunner.runCommand(
|
||||
name,
|
||||
command,
|
||||
config.clientLedgerApi,
|
||||
config.token,
|
||||
)
|
||||
|
||||
override protected[console] def token: Option[String] = config.token
|
||||
|
||||
private lazy val testing_ = new ParticipantTestingGroup(this, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Commands used for development and testing", FeatureFlag.Testing)
|
||||
@Help.Group("Testing")
|
||||
override def testing: ParticipantTestingGroup = testing_
|
||||
|
||||
private lazy val repair_ =
|
||||
new ParticipantRepairAdministration(consoleEnvironment, this, loggerFactory)
|
||||
|
||||
@Help.Summary("Commands to repair the participant contract state", FeatureFlag.Repair)
|
||||
@Help.Group("Repair")
|
||||
def repair: ParticipantRepairAdministration = repair_
|
||||
}
|
||||
|
||||
class RemoteParticipantReference(environment: ConsoleEnvironment, override val name: String)
|
||||
extends ParticipantReference(environment, name)
|
||||
with GrpcRemoteInstanceReference
|
||||
with RemoteParticipantReferenceCommon {
|
||||
|
||||
@Help.Summary("Inspect and manage parties")
|
||||
@Help.Group("Parties")
|
||||
def parties: ParticipantPartiesAdministrationGroup = partiesGroup
|
||||
|
||||
// above command needs to be def such that `Help` works.
|
||||
lazy private val partiesGroup =
|
||||
new ParticipantPartiesAdministrationGroup(id, this, consoleEnvironment)
|
||||
|
||||
@Help.Summary("Return remote participant config")
|
||||
def config: RemoteParticipantConfig =
|
||||
consoleEnvironment.environment.config.remoteParticipantsByString(name)
|
||||
|
||||
override def equals(obj: Any): Boolean = {
|
||||
obj match {
|
||||
case x: RemoteParticipantReference =>
|
||||
x.consoleEnvironment == consoleEnvironment && x.name == name
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed trait LocalParticipantReferenceCommon
|
||||
extends LedgerApiCommandRunner
|
||||
with ParticipantReferenceCommon
|
||||
with LocalInstanceReferenceCommon {
|
||||
|
||||
def config: LocalParticipantConfig
|
||||
|
||||
def adminToken: Option[String]
|
||||
|
||||
override protected[console] def ledgerApiCommand[Result](
|
||||
command: GrpcAdminCommand[_, _, Result]
|
||||
): ConsoleCommandResult[Result] =
|
||||
runCommandIfRunning(
|
||||
consoleEnvironment.grpcAdminCommandRunner
|
||||
.runCommand(name, command, config.clientLedgerApi, adminToken)
|
||||
)
|
||||
|
||||
override protected[console] def token: Option[String] = adminToken
|
||||
|
||||
@Help.Summary("Commands to repair the local participant contract state", FeatureFlag.Repair)
|
||||
@Help.Group("Repair")
|
||||
def repair: LocalParticipantRepairAdministration
|
||||
}
|
||||
|
||||
class LocalParticipantReference(
|
||||
override val consoleEnvironment: ConsoleEnvironment,
|
||||
name: String,
|
||||
) extends ParticipantReference(consoleEnvironment, name)
|
||||
with LocalParticipantReferenceCommon
|
||||
with LocalInstanceReference
|
||||
with BaseInspection[ParticipantNode] {
|
||||
|
||||
override private[console] val nodes = consoleEnvironment.environment.participants
|
||||
|
||||
@Help.Summary("Return participant config")
|
||||
def config: LocalParticipantConfig =
|
||||
consoleEnvironment.environment.config.participantsByString(name)
|
||||
|
||||
private lazy val testing_ =
|
||||
new LocalParticipantTestingGroup(this, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Commands used for development and testing", FeatureFlag.Testing)
|
||||
override def testing: LocalParticipantTestingGroup = testing_
|
||||
|
||||
private lazy val commitments_ =
|
||||
new LocalCommitmentsAdministrationGroup(this, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Commands to inspect and extract bilateral commitments", FeatureFlag.Preview)
|
||||
@Help.Group("Commitments")
|
||||
def commitments: LocalCommitmentsAdministrationGroup = commitments_
|
||||
|
||||
private lazy val repair_ =
|
||||
new LocalParticipantRepairAdministration(consoleEnvironment, this, loggerFactory) {
|
||||
override protected def access[T](handler: ParticipantNodeCommon => T): T =
|
||||
LocalParticipantReference.this.access(handler)
|
||||
}
|
||||
@Help.Summary("Commands to repair the local participant contract state", FeatureFlag.Repair)
|
||||
@Help.Group("Repair")
|
||||
def repair: LocalParticipantRepairAdministration = repair_
|
||||
|
||||
@Help.Summary("Inspect and manage parties")
|
||||
@Help.Group("Parties")
|
||||
override def parties: LocalParticipantPartiesAdministrationGroup = partiesGroup
|
||||
// above command needs to be def such that `Help` works.
|
||||
lazy private val partiesGroup =
|
||||
new LocalParticipantPartiesAdministrationGroup(this, this, consoleEnvironment, loggerFactory)
|
||||
|
||||
/** secret, not publicly documented way to get the admin token */
|
||||
def adminToken: Option[String] = underlying.map(_.adminToken.secret)
|
||||
|
||||
override def equals(obj: Any): Boolean = {
|
||||
obj match {
|
||||
case x: LocalParticipantReference =>
|
||||
x.consoleEnvironment == consoleEnvironment && x.name == name
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
override def runningNode: Option[CantonNodeBootstrap[ParticipantNode]] =
|
||||
consoleEnvironment.environment.participants.getRunning(name)
|
||||
|
||||
override def startingNode: Option[CantonNodeBootstrap[ParticipantNode]] =
|
||||
consoleEnvironment.environment.participants.getStarting(name)
|
||||
|
||||
}
|
||||
|
||||
abstract class ParticipantReferenceX(
|
||||
override val consoleEnvironment: ConsoleEnvironment,
|
||||
val name: String,
|
||||
) extends ParticipantReferenceCommon
|
||||
with InstanceReferenceX {
|
||||
|
||||
override protected val instanceType: String = ParticipantReferenceX.InstanceType
|
||||
override protected def runner: AdminCommandRunner = this
|
||||
|
||||
@Help.Summary("Health and diagnostic related commands")
|
||||
@Help.Group("Health")
|
||||
override def health: ParticipantHealthAdministrationX =
|
||||
new ParticipantHealthAdministrationX(this, consoleEnvironment, loggerFactory)
|
||||
|
||||
@Help.Summary("Inspect and manage parties")
|
||||
@Help.Group("Parties")
|
||||
def parties: ParticipantPartiesAdministrationGroupX
|
||||
|
||||
private lazy val topology_ =
|
||||
new TopologyAdministrationGroupX(
|
||||
this,
|
||||
health.status.successOption.map(_.topologyQueue),
|
||||
consoleEnvironment,
|
||||
loggerFactory,
|
||||
)
|
||||
@Help.Summary("Topology management related commands")
|
||||
@Help.Group("Topology")
|
||||
@Help.Description("This group contains access to the full set of topology management commands.")
|
||||
override def topology: TopologyAdministrationGroupX = topology_
|
||||
override protected def vettedPackagesOfParticipant(): Set[PackageId] = topology.vetted_packages
|
||||
.list(filterStore = "Authorized", filterParticipant = id.filterString)
|
||||
.flatMap(_.item.packageIds)
|
||||
.toSet
|
||||
override protected def participantIsActiveOnDomain(
|
||||
domainId: DomainId,
|
||||
participantId: ParticipantId,
|
||||
): Boolean = topology.domain_trust_certificates.active(domainId, participantId)
|
||||
|
||||
}
|
||||
object ParticipantReferenceX {
|
||||
val InstanceType = "ParticipantX"
|
||||
}
|
||||
|
||||
class RemoteParticipantReferenceX(environment: ConsoleEnvironment, override val name: String)
|
||||
extends ParticipantReferenceX(environment, name)
|
||||
with GrpcRemoteInstanceReference
|
||||
with RemoteParticipantReferenceCommon {
|
||||
|
||||
@Help.Summary("Inspect and manage parties")
|
||||
@Help.Group("Parties")
|
||||
override def parties: ParticipantPartiesAdministrationGroupX = partiesGroup
|
||||
|
||||
// above command needs to be def such that `Help` works.
|
||||
lazy private val partiesGroup =
|
||||
new ParticipantPartiesAdministrationGroupX(id, this, consoleEnvironment)
|
||||
|
||||
@Help.Summary("Return remote participant config")
|
||||
def config: RemoteParticipantConfig =
|
||||
consoleEnvironment.environment.config.remoteParticipantsByStringX(name)
|
||||
|
||||
override def equals(obj: Any): Boolean = {
|
||||
obj match {
|
||||
case x: RemoteParticipantReference =>
|
||||
x.consoleEnvironment == consoleEnvironment && x.name == name
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LocalParticipantReferenceX(
|
||||
override val consoleEnvironment: ConsoleEnvironment,
|
||||
name: String,
|
||||
) extends ParticipantReferenceX(consoleEnvironment, name)
|
||||
with LocalParticipantReferenceCommon
|
||||
with LocalInstanceReferenceX
|
||||
with BaseInspection[ParticipantNodeX] {
|
||||
|
||||
override private[console] val nodes = consoleEnvironment.environment.participantsX
|
||||
|
||||
@Help.Summary("Return participant config")
|
||||
def config: LocalParticipantConfig =
|
||||
consoleEnvironment.environment.config.participantsByStringX(name)
|
||||
|
||||
override def runningNode: Option[ParticipantNodeBootstrapX] =
|
||||
consoleEnvironment.environment.participantsX.getRunning(name)
|
||||
|
||||
override def startingNode: Option[ParticipantNodeBootstrapX] =
|
||||
consoleEnvironment.environment.participantsX.getStarting(name)
|
||||
|
||||
/** secret, not publicly documented way to get the admin token */
|
||||
def adminToken: Option[String] = underlying.map(_.adminToken.secret)
|
||||
|
||||
// TODO(#14048) these are "remote" groups. the normal participant node has "local" versions.
|
||||
// but rather than keeping this, we should make local == remote and add local methods separately
|
||||
@Help.Summary("Inspect and manage parties")
|
||||
@Help.Group("Parties")
|
||||
def parties: LocalParticipantPartiesAdministrationGroupX = partiesGroup
|
||||
// above command needs to be def such that `Help` works.
|
||||
lazy private val partiesGroup =
|
||||
new LocalParticipantPartiesAdministrationGroupX(this, this, consoleEnvironment, loggerFactory)
|
||||
|
||||
private lazy val testing_ = new ParticipantTestingGroup(this, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Commands used for development and testing", FeatureFlag.Testing)
|
||||
@Help.Group("Testing")
|
||||
override def testing: ParticipantTestingGroup = testing_
|
||||
|
||||
private lazy val repair_ =
|
||||
new LocalParticipantRepairAdministration(consoleEnvironment, this, loggerFactory) {
|
||||
override protected def access[T](handler: ParticipantNodeCommon => T): T =
|
||||
LocalParticipantReferenceX.this.access(handler)
|
||||
}
|
||||
|
||||
@Help.Summary("Commands to repair the local participant contract state", FeatureFlag.Repair)
|
||||
@Help.Group("Repair")
|
||||
def repair: LocalParticipantRepairAdministration = repair_
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import com.digitalasset.canton.environment.{CantonNode, CantonNodeBootstrap, Nodes}
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
|
||||
/** Aliases to manage a sequence of instances in a REPL environment
|
||||
*/
|
||||
trait LocalInstancesExtensions extends Helpful {
|
||||
|
||||
import ConsoleCommandResult.runAll
|
||||
|
||||
def instances: Seq[LocalInstanceReferenceCommon]
|
||||
|
||||
@Help.Summary("Database management related operations")
|
||||
@Help.Group("Database")
|
||||
object db extends Helpful {
|
||||
|
||||
@Help.Summary("Migrate all databases")
|
||||
def migrate()(implicit consoleEnvironment: ConsoleEnvironment): Unit = {
|
||||
val _ = runAll(instances.sorted(consoleEnvironment.startupOrdering)) {
|
||||
_.migrateDbCommand()
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Only use when advised - repair the database migration of all nodes")
|
||||
@Help.Description(
|
||||
"""In some rare cases, we change already applied database migration files in a new release and the repair
|
||||
|command resets the checksums we use to ensure that in general already applied migration files have not been changed.
|
||||
|You should only use `db.repair_migration` when advised and otherwise use it at your own risk - in the worst case running
|
||||
|it may lead to data corruption when an incompatible database migration (one that should be rejected because
|
||||
|the already applied database migration files have changed) is subsequently falsely applied.
|
||||
|"""
|
||||
)
|
||||
def repair_migration(
|
||||
force: Boolean = false
|
||||
)(implicit consoleEnvironment: ConsoleEnvironment): Unit = {
|
||||
val _ = runAll(instances.sorted(consoleEnvironment.startupOrdering)) {
|
||||
_.repairMigrationCommand(force)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def runOnAllInstances[T](
|
||||
cmd: Seq[(String, Nodes[CantonNode, CantonNodeBootstrap[CantonNode]])] => Either[T, Unit]
|
||||
)(implicit consoleEnvironment: ConsoleEnvironment): Unit =
|
||||
consoleEnvironment.runE(cmd(instances.map(x => (x.name, x.nodes))))
|
||||
|
||||
@Help.Summary("Start all")
|
||||
def start()(implicit consoleEnvironment: ConsoleEnvironment): Unit =
|
||||
TraceContext.withNewTraceContext { implicit traceContext =>
|
||||
runOnAllInstances(consoleEnvironment.environment.startNodes(_))
|
||||
}
|
||||
|
||||
@Help.Summary("Stop all")
|
||||
def stop()(implicit consoleEnvironment: ConsoleEnvironment): Unit =
|
||||
TraceContext.withNewTraceContext { implicit traceContext =>
|
||||
runOnAllInstances(consoleEnvironment.environment.stopNodes(_))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object LocalInstancesExtensions {
|
||||
class Impl(val instances: Seq[LocalInstanceReferenceCommon]) extends LocalInstancesExtensions {}
|
||||
}
|
||||
|
||||
class LocalDomainReferencesExtensions(domains: Seq[LocalDomainReference])
|
||||
extends LocalInstancesExtensions {
|
||||
|
||||
override def instances: Seq[LocalDomainReference] = domains
|
||||
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import cats.syntax.traverse.*
|
||||
import com.daml.nonempty.NonEmpty
|
||||
import com.digitalasset.canton.config.NonNegativeDuration
|
||||
import com.digitalasset.canton.console.commands.ParticipantCommands
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.participant.domain.DomainConnectionConfig
|
||||
import com.digitalasset.canton.{DomainAlias, SequencerAlias}
|
||||
|
||||
class ParticipantReferencesExtensions(participants: Seq[ParticipantReferenceCommon])(implicit
|
||||
override val consoleEnvironment: ConsoleEnvironment
|
||||
) extends Helpful
|
||||
with NamedLogging
|
||||
with FeatureFlagFilter {
|
||||
|
||||
protected override def loggerFactory: NamedLoggerFactory =
|
||||
consoleEnvironment.environment.loggerFactory
|
||||
|
||||
@Help.Summary("Manage dars on several participants at once")
|
||||
@Help.Group("DAR Management")
|
||||
object dars extends Helpful {
|
||||
@Help.Summary("Upload DARs to participants")
|
||||
@Help.Description(
|
||||
"""If vetAllPackages is true, the participants will vet the package on all domains they are registered.
|
||||
If synchronizeVetting is true, the command will block until the package vetting transaction has been registered with all connected domains."""
|
||||
)
|
||||
def upload(
|
||||
darPath: String,
|
||||
vetAllPackages: Boolean = true,
|
||||
synchronizeVetting: Boolean = true,
|
||||
): Map[ParticipantReferenceCommon, String] = {
|
||||
val res = ConsoleCommandResult.runAll(participants)(
|
||||
ParticipantCommands.dars
|
||||
.upload(
|
||||
_,
|
||||
darPath,
|
||||
vetAllPackages = vetAllPackages,
|
||||
synchronizeVetting = synchronizeVetting,
|
||||
logger,
|
||||
)
|
||||
)
|
||||
if (synchronizeVetting && vetAllPackages) {
|
||||
participants.foreach(_.packages.synchronize_vetting())
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Manage domain connections on several participants at once")
|
||||
@Help.Group("Domains")
|
||||
object domains extends Helpful {
|
||||
|
||||
@Help.Summary("Disconnect from domain")
|
||||
def disconnect(alias: DomainAlias): Unit =
|
||||
ConsoleCommandResult
|
||||
.runAll(participants)(ParticipantCommands.domains.disconnect(_, alias))
|
||||
.discard
|
||||
|
||||
@Help.Summary("Disconnect from a local domain")
|
||||
def disconnect_local(domain: LocalDomainReference): Unit =
|
||||
ConsoleCommandResult
|
||||
.runAll(participants)(
|
||||
ParticipantCommands.domains.disconnect(_, DomainAlias.tryCreate(domain.name))
|
||||
)
|
||||
.discard
|
||||
|
||||
@Help.Summary("Reconnect to domain")
|
||||
@Help.Description(
|
||||
"If retry is set to true (default), the command will return after the first attempt, but keep on trying in the background."
|
||||
)
|
||||
def reconnect(alias: DomainAlias, retry: Boolean = true): Unit =
|
||||
ConsoleCommandResult
|
||||
.runAll(participants)(
|
||||
ParticipantCommands.domains.reconnect(_, alias, retry)
|
||||
)
|
||||
.discard
|
||||
|
||||
@Help.Summary("Reconnect to all domains for which `manualStart` = false")
|
||||
@Help.Description(
|
||||
"""If ignoreFailures is set to true (default), the reconnect all will succeed even if some domains are offline.
|
||||
| The participants will continue attempting to establish a domain connection."""
|
||||
)
|
||||
def reconnect_all(ignoreFailures: Boolean = true): Unit = {
|
||||
val _ = ConsoleCommandResult.runAll(participants)(
|
||||
ParticipantCommands.domains.reconnect_all(_, ignoreFailures = ignoreFailures)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Disconnect from all connected domains")
|
||||
def disconnect_all(): Unit =
|
||||
ConsoleCommandResult
|
||||
.runAll(participants) { p =>
|
||||
ConsoleCommandResult.fromEither(for {
|
||||
connected <- ParticipantCommands.domains.list_connected(p).toEither
|
||||
_ <- connected
|
||||
.traverse(d => ParticipantCommands.domains.disconnect(p, d.domainAlias).toEither)
|
||||
} yield ())
|
||||
}
|
||||
.discard
|
||||
|
||||
@Help.Summary("Register and potentially connect to domain")
|
||||
def register(config: DomainConnectionConfig): Unit =
|
||||
ConsoleCommandResult
|
||||
.runAll(participants)(ParticipantCommands.domains.register(_, config))
|
||||
.discard
|
||||
|
||||
@Help.Summary("Register and potentially connect to new local domain")
|
||||
@Help.Description("""
|
||||
The arguments are:
|
||||
domain - A local domain or sequencer reference
|
||||
manualConnect - Whether this connection should be handled manually and also excluded from automatic re-connect.
|
||||
synchronize - A timeout duration indicating how long to wait for all topology changes to have been effected on all local nodes.
|
||||
""")
|
||||
def connect_local(
|
||||
domain: InstanceReferenceWithSequencerConnection,
|
||||
manualConnect: Boolean = false,
|
||||
alias: Option[DomainAlias] = None,
|
||||
synchronize: Option[NonNegativeDuration] = Some(
|
||||
consoleEnvironment.commandTimeouts.bounded
|
||||
),
|
||||
): Unit = {
|
||||
val config =
|
||||
ParticipantCommands.domains.referenceToConfig(
|
||||
NonEmpty.mk(Seq, SequencerAlias.Default -> domain).toMap,
|
||||
manualConnect,
|
||||
alias,
|
||||
)
|
||||
register(config)
|
||||
synchronize.foreach { timeout =>
|
||||
ConsoleMacros.utils.synchronize_topology(Some(timeout))(consoleEnvironment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class LocalParticipantReferencesExtensions(participants: Seq[LocalParticipantReference])(implicit
|
||||
override val consoleEnvironment: ConsoleEnvironment
|
||||
) extends ParticipantReferencesExtensions(participants)
|
||||
with LocalInstancesExtensions {
|
||||
override def instances: Seq[LocalInstanceReferenceCommon] = participants
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
ConsoleEnvironment,
|
||||
FeatureFlagFilter,
|
||||
Helpful,
|
||||
}
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
|
||||
trait ConsoleCommandGroup extends Helpful with FeatureFlagFilter with NamedLogging {
|
||||
protected def runner: AdminCommandRunner
|
||||
protected def consoleEnvironment: ConsoleEnvironment
|
||||
private[commands] def myLoggerFactory: NamedLoggerFactory = loggerFactory
|
||||
}
|
||||
|
||||
object ConsoleCommandGroup {
|
||||
class Impl(parent: ConsoleCommandGroup) extends ConsoleCommandGroup {
|
||||
override protected def consoleEnvironment: ConsoleEnvironment = parent.consoleEnvironment
|
||||
override protected def runner: AdminCommandRunner = parent.runner
|
||||
override protected def loggerFactory: NamedLoggerFactory = parent.myLoggerFactory
|
||||
}
|
||||
}
|
@ -0,0 +1,438 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import com.digitalasset.canton.DiscardOps
|
||||
import com.digitalasset.canton.admin.api.client.commands.DomainAdminCommands.GetDomainParameters
|
||||
import com.digitalasset.canton.admin.api.client.commands.{
|
||||
DomainAdminCommands,
|
||||
TopologyAdminCommands,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.data.{
|
||||
DynamicDomainParameters,
|
||||
ListParticipantDomainStateResult,
|
||||
StaticDomainParameters,
|
||||
}
|
||||
import com.digitalasset.canton.config.RequireTypes.NonNegativeInt
|
||||
import com.digitalasset.canton.config.{
|
||||
ConsoleCommandTimeout,
|
||||
NonNegativeDuration,
|
||||
NonNegativeFiniteDuration,
|
||||
PositiveDurationSeconds,
|
||||
}
|
||||
import com.digitalasset.canton.console.CommandErrors.GenericCommandError
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
ConsoleEnvironment,
|
||||
FeatureFlagFilter,
|
||||
Help,
|
||||
Helpful,
|
||||
}
|
||||
import com.digitalasset.canton.domain.service.ServiceAgreementAcceptance
|
||||
import com.digitalasset.canton.error.CantonError
|
||||
import com.digitalasset.canton.health.admin.data.NodeStatus
|
||||
import com.digitalasset.canton.logging.NamedLogging
|
||||
import com.digitalasset.canton.time.EnrichedDurations.*
|
||||
import com.digitalasset.canton.topology.TopologyManagerError.IncreaseOfLedgerTimeRecordTimeTolerance
|
||||
import com.digitalasset.canton.topology.*
|
||||
import com.digitalasset.canton.topology.admin.grpc.BaseQuery
|
||||
import com.digitalasset.canton.topology.store.{TimeQuery, TopologyStoreId}
|
||||
import com.digitalasset.canton.topology.transaction.*
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.digitalasset.canton.util.ShowUtil.*
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
import java.time.Duration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.math.Ordering.Implicits.infixOrderingOps
|
||||
import scala.util.chaining.scalaUtilChainingOps
|
||||
|
||||
trait DomainAdministration {
|
||||
this: AdminCommandRunner with FeatureFlagFilter with NamedLogging =>
|
||||
protected val consoleEnvironment: ConsoleEnvironment
|
||||
|
||||
def id: DomainId
|
||||
def topology: TopologyAdministrationGroup
|
||||
protected def timeouts: ConsoleCommandTimeout = consoleEnvironment.commandTimeouts
|
||||
|
||||
type Status <: NodeStatus.Status
|
||||
def health: HealthAdministration[Status]
|
||||
|
||||
// The DomainTopologyTransactionMessage is about 2500 bytes and each recipient about 100 bytes.
|
||||
// with this minimum we can have up to 275 recipients for a domain transaction change.
|
||||
private val minimumMaxRequestSizeBytes = NonNegativeInt.tryCreate(30000)
|
||||
|
||||
@Help.Summary("Manage participant permissions")
|
||||
@Help.Group("Participants")
|
||||
object participants extends Helpful {
|
||||
|
||||
@Help.Summary("List participant states")
|
||||
@Help.Description(
|
||||
"""This command will list the currently valid state as stored in the authorized store.
|
||||
| For a deep inspection of the identity management history, use the `topology.participant_domain_states.list` command."""
|
||||
)
|
||||
def list(): Seq[ListParticipantDomainStateResult] = {
|
||||
consoleEnvironment
|
||||
.run {
|
||||
adminCommand(
|
||||
TopologyAdminCommands.Read.ListParticipantDomainState(
|
||||
BaseQuery(
|
||||
filterStore = TopologyStoreId.AuthorizedStore.filterName,
|
||||
useStateStore = false,
|
||||
ops = None,
|
||||
timeQuery = TimeQuery.HeadState,
|
||||
filterSigningKey = "",
|
||||
protocolVersion = None,
|
||||
),
|
||||
filterDomain = "",
|
||||
filterParticipant = "",
|
||||
)
|
||||
)
|
||||
}
|
||||
.filter(_.item.side != RequestSide.To)
|
||||
}
|
||||
|
||||
@Help.Summary("Change state and trust level of participant")
|
||||
@Help.Description("""Set the state of the participant within the domain.
|
||||
Valid permissions are 'Submission', 'Confirmation', 'Observation' and 'Disabled'.
|
||||
Valid trust levels are 'Vip' and 'Ordinary'.
|
||||
Synchronize timeout can be used to ensure that the state has been propagated into the node
|
||||
""")
|
||||
def set_state(
|
||||
participant: ParticipantId,
|
||||
permission: ParticipantPermission,
|
||||
trustLevel: TrustLevel = TrustLevel.Ordinary,
|
||||
synchronize: Option[NonNegativeDuration] = Some(timeouts.bounded),
|
||||
): Unit = {
|
||||
val _ = consoleEnvironment.run {
|
||||
adminCommand(
|
||||
TopologyAdminCommands.Write.AuthorizeParticipantDomainState(
|
||||
TopologyChangeOp.Add,
|
||||
None,
|
||||
RequestSide.From,
|
||||
id,
|
||||
participant,
|
||||
permission,
|
||||
trustLevel,
|
||||
replaceExisting = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
synchronize.foreach(topology.synchronisation.await_idle)
|
||||
}
|
||||
|
||||
@Help.Summary("Test whether a participant is permissioned on this domain")
|
||||
def active(participantId: ParticipantId): Boolean =
|
||||
topology.participant_domain_states.active(id, participantId)
|
||||
}
|
||||
|
||||
@Help.Summary("Domain service commands")
|
||||
@Help.Group("Service")
|
||||
object service extends Helpful {
|
||||
|
||||
@Help.Summary("List the accepted service agreements")
|
||||
def list_accepted_agreements(): Seq[ServiceAgreementAcceptance] =
|
||||
consoleEnvironment.run(adminCommand(DomainAdminCommands.ListAcceptedServiceAgreements))
|
||||
|
||||
@Help.Summary("Get the Static Domain Parameters configured for the domain")
|
||||
def get_static_domain_parameters: StaticDomainParameters =
|
||||
consoleEnvironment.run(
|
||||
adminCommand(GetDomainParameters())
|
||||
)
|
||||
|
||||
@Help.Summary("Get the Dynamic Domain Parameters configured for the domain")
|
||||
def get_dynamic_domain_parameters: DynamicDomainParameters = topology.domain_parameters_changes
|
||||
.list("Authorized")
|
||||
.sortBy(_.context.validFrom)(implicitly[Ordering[java.time.Instant]].reverse)
|
||||
.headOption
|
||||
.map(_.item)
|
||||
.getOrElse(
|
||||
throw new IllegalStateException("No dynamic domain parameters found in the domain")
|
||||
)
|
||||
|
||||
@Help.Summary("Get the reconciliation interval configured for the domain")
|
||||
@Help.Description("""Depending on the protocol version used on the domain, the value will be
|
||||
read either from the static domain parameters or the dynamic ones.""")
|
||||
def get_reconciliation_interval: PositiveDurationSeconds =
|
||||
get_dynamic_domain_parameters.reconciliationInterval
|
||||
|
||||
@Help.Summary("Get the max rate per participant")
|
||||
@Help.Description("""Depending on the protocol version used on the domain, the value will be
|
||||
read either from the static domain parameters or the dynamic ones.""")
|
||||
def get_max_rate_per_participant: NonNegativeInt =
|
||||
get_dynamic_domain_parameters.maxRatePerParticipant
|
||||
|
||||
@Help.Summary("Get the max request size")
|
||||
@Help.Description("""Depending on the protocol version used on the domain, the value will be
|
||||
read either from the static domain parameters or the dynamic ones.
|
||||
This value is not necessarily the one used by the sequencer node because it requires a restart
|
||||
of the server to be taken into account.""")
|
||||
def get_max_request_size: NonNegativeInt =
|
||||
TraceContext.withNewTraceContext { implicit tc =>
|
||||
get_dynamic_domain_parameters.maxRequestSize.tap { res =>
|
||||
logger.info(
|
||||
s"This value ($res) is not necessarily the one used by the sequencer node because it requires a restart of the server to be taken into account"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Get the mediator deduplication timeout")
|
||||
@Help.Description(
|
||||
"The method will fail, if the domain does not support the mediatorDeduplicationTimeout."
|
||||
)
|
||||
def get_mediator_deduplication_timeout: NonNegativeFiniteDuration =
|
||||
get_dynamic_domain_parameters.mediatorDeduplicationTimeout
|
||||
|
||||
@Help.Summary("Update the mediator deduplication timeout")
|
||||
@Help.Description(
|
||||
"""The method will fail:
|
||||
|
|
||||
|- if the domain does not support the ``mediatorDeduplicationTimeout`` parameter,
|
||||
|- if the new value of ``mediatorDeduplicationTimeout`` is less than twice the value of ``ledgerTimeRecordTimeTolerance.``"""
|
||||
)
|
||||
def set_mediator_deduplication_timeout(
|
||||
newMediatorDeduplicationTimeout: NonNegativeFiniteDuration
|
||||
): Unit =
|
||||
update_dynamic_domain_parameters(
|
||||
_.copy(mediatorDeduplicationTimeout = newMediatorDeduplicationTimeout)
|
||||
)
|
||||
|
||||
@Help.Summary("Set the Dynamic Domain Parameters configured for the domain")
|
||||
@Help.Description(
|
||||
"""force: Enable potentially dangerous changes. Required to increase ``ledgerTimeRecordTimeTolerance``.
|
||||
|Use ``set_ledger_time_record_time_tolerance`` to securely increase ``ledgerTimeRecordTimeTolerance``."""
|
||||
)
|
||||
def set_dynamic_domain_parameters(
|
||||
dynamicDomainParameters: DynamicDomainParameters,
|
||||
force: Boolean = false,
|
||||
): Unit = {
|
||||
val protocolVersion = get_static_domain_parameters.protocolVersion
|
||||
topology.domain_parameters_changes
|
||||
.authorize(id, dynamicDomainParameters, protocolVersion, force = force)
|
||||
.discard[ByteString]
|
||||
}
|
||||
|
||||
@Help.Summary("Update the Dynamic Domain Parameters for the domain")
|
||||
@Help.Description(
|
||||
"""force: Enable potentially dangerous changes. Required to increase ``ledgerTimeRecordTimeTolerance``.
|
||||
|Use ``set_ledger_time_record_time_tolerance_securely`` to securely increase ``ledgerTimeRecordTimeTolerance``."""
|
||||
)
|
||||
def update_dynamic_domain_parameters(
|
||||
modifier: DynamicDomainParameters => DynamicDomainParameters,
|
||||
force: Boolean = false,
|
||||
): Unit = {
|
||||
val currentDomainParameters = get_dynamic_domain_parameters
|
||||
val protocolVersion = get_static_domain_parameters.protocolVersion
|
||||
|
||||
val newDomainParameters = modifier(currentDomainParameters)
|
||||
topology.domain_parameters_changes
|
||||
.authorize(id, newDomainParameters, protocolVersion, force = force)
|
||||
.discard[ByteString]
|
||||
}
|
||||
|
||||
@Help.Summary("Try to update the reconciliation interval for the domain")
|
||||
@Help.Description("""If the reconciliation interval is dynamic, update the value.
|
||||
If the reconciliation interval is not dynamic (i.e., if the domain is running
|
||||
on protocol version lower than `4`), then it will throw an error.
|
||||
""")
|
||||
def set_reconciliation_interval(
|
||||
newReconciliationInterval: PositiveDurationSeconds
|
||||
): Unit =
|
||||
update_dynamic_domain_parameters(
|
||||
_.copy(reconciliationInterval = newReconciliationInterval)
|
||||
)
|
||||
|
||||
@Help.Summary("Try to update the max rate per participant for the domain")
|
||||
@Help.Description("""If the max rate per participant is dynamic, update the value.
|
||||
If the max rate per participant is not dynamic (i.e., if the domain is running
|
||||
on protocol version lower than `4`), then it will throw an error.
|
||||
""")
|
||||
def set_max_rate_per_participant(
|
||||
maxRatePerParticipant: NonNegativeInt
|
||||
): Unit =
|
||||
update_dynamic_domain_parameters(_.copy(maxRatePerParticipant = maxRatePerParticipant))
|
||||
|
||||
@Help.Summary("Try to update the max rate per participant for the domain")
|
||||
@Help.Description("""If the max request size is dynamic, update the value.
|
||||
The update won't have any effect unless the sequencer server is restarted.
|
||||
If the max request size is not dynamic (i.e., if the domain is running
|
||||
on protocol version lower than `4`), then it will throw an error.
|
||||
""")
|
||||
def set_max_request_size(
|
||||
maxRequestSize: NonNegativeInt,
|
||||
force: Boolean = false,
|
||||
): Unit =
|
||||
TraceContext.withNewTraceContext { implicit tc =>
|
||||
if (maxRequestSize < minimumMaxRequestSizeBytes && !force)
|
||||
logger.warn(
|
||||
s"""|The maxRequestSize requested is lower than the minimum advised value ($minimumMaxRequestSizeBytes) which can crash Canton.
|
||||
|To set this value anyway, set force to true.""".stripMargin
|
||||
)
|
||||
else
|
||||
update_dynamic_domain_parameters(_.copy(maxRequestSize = maxRequestSize))
|
||||
logger.info(
|
||||
"Please restart the sequencer node to take into account the new value for max-request-size."
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary(
|
||||
"Update the `ledgerTimeRecordTimeTolerance` in the dynamic domain parameters."
|
||||
)
|
||||
@Help.Description(
|
||||
"""If it would be insecure to perform the change immediately,
|
||||
|the command will block and wait until it is secure to perform the change.
|
||||
|The command will block for at most twice of ``newLedgerTimeRecordTimeTolerance``.
|
||||
|
|
||||
|If the domain does not support ``mediatorDeduplicationTimeout``,
|
||||
|the method will update ``ledgerTimeRecordTimeTolerance`` immediately without blocking.
|
||||
|
|
||||
|The method will fail if ``mediatorDeduplicationTimeout`` is less than twice of ``newLedgerTimeRecordTimeTolerance``.
|
||||
|
|
||||
|Do not modify domain parameters concurrently while running this command,
|
||||
|because the command may override concurrent changes.
|
||||
|
|
||||
|force: update ``ledgerTimeRecordTimeTolerance`` immediately without blocking.
|
||||
|This is safe to do during domain bootstrapping and in test environments, but should not be done in operational production systems.."""
|
||||
)
|
||||
def set_ledger_time_record_time_tolerance(
|
||||
newLedgerTimeRecordTimeTolerance: NonNegativeFiniteDuration,
|
||||
force: Boolean = false,
|
||||
): Unit = {
|
||||
TraceContext.withNewTraceContext { implicit tc =>
|
||||
get_dynamic_domain_parameters match {
|
||||
case oldDomainParameters: DynamicDomainParameters if !force =>
|
||||
securely_set_ledger_time_record_time_tolerance(
|
||||
oldDomainParameters,
|
||||
newLedgerTimeRecordTimeTolerance,
|
||||
)
|
||||
|
||||
case _: DynamicDomainParameters =>
|
||||
logger.info(
|
||||
s"Immediately updating ledgerTimeRecordTimeTolerance to $newLedgerTimeRecordTimeTolerance..."
|
||||
)
|
||||
update_dynamic_domain_parameters(
|
||||
_.update(ledgerTimeRecordTimeTolerance = newLedgerTimeRecordTimeTolerance),
|
||||
force = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def securely_set_ledger_time_record_time_tolerance(
|
||||
oldDomainParameters: DynamicDomainParameters,
|
||||
newLedgerTimeRecordTimeTolerance: NonNegativeFiniteDuration,
|
||||
)(implicit traceContext: TraceContext): Unit = {
|
||||
implicit val ec: ExecutionContext = consoleEnvironment.environment.executionContext
|
||||
|
||||
// See i9028 for a detailed design.
|
||||
// https://docs.google.com/document/d/1tpPbzv2s6bjbekVGBn6X5VZuw0oOTHek5c30CBo4UkI/edit#bookmark=id.1dzc6dxxlpca
|
||||
// We wait until the antecedent of Lemma 2 Item 2 is falsified for all changes that violate the conclusion.
|
||||
|
||||
// Compute new parameters
|
||||
val oldLedgerTimeRecordTimeTolerance = oldDomainParameters.ledgerTimeRecordTimeTolerance
|
||||
|
||||
val minMediatorDeduplicationTimeout = newLedgerTimeRecordTimeTolerance * 2
|
||||
|
||||
if (oldDomainParameters.mediatorDeduplicationTimeout < minMediatorDeduplicationTimeout) {
|
||||
val err = IncreaseOfLedgerTimeRecordTimeTolerance
|
||||
.PermanentlyInsecure(
|
||||
newLedgerTimeRecordTimeTolerance.toInternal,
|
||||
oldDomainParameters.mediatorDeduplicationTimeout.toInternal,
|
||||
)
|
||||
val msg = CantonError.stringFromContext(err)
|
||||
consoleEnvironment.run(GenericCommandError(msg))
|
||||
}
|
||||
|
||||
logger.info(
|
||||
s"Securely updating ledgerTimeRecordTimeTolerance to $newLedgerTimeRecordTimeTolerance..."
|
||||
)
|
||||
|
||||
// Poll until it is safe to increase ledgerTimeRecordTimeTolerance
|
||||
def checkPreconditions(): Future[Unit] = {
|
||||
val startTs = consoleEnvironment.environment.clock.now
|
||||
|
||||
// Update mediatorDeduplicationTimeout for several reasons:
|
||||
// 1. Make sure it is big enough.
|
||||
// 2. The resulting topology transaction gives us a meaningful lower bound on the sequencer clock.
|
||||
logger.info(
|
||||
s"Do a no-op update of ledgerTimeRecordTimeTolerance to $oldLedgerTimeRecordTimeTolerance..."
|
||||
)
|
||||
update_dynamic_domain_parameters(
|
||||
_.copy(ledgerTimeRecordTimeTolerance = oldLedgerTimeRecordTimeTolerance)
|
||||
)
|
||||
|
||||
logger.debug(s"Check for incompatible past domain parameters...")
|
||||
|
||||
val allTransactions = topology.domain_parameters_changes.list(
|
||||
id.filterString,
|
||||
useStateStore = false,
|
||||
// We can't specify a lower bound in range because that would be compared against validFrom.
|
||||
// (But we need to compare to validUntil).
|
||||
TimeQuery.Range(None, None),
|
||||
)
|
||||
|
||||
// This serves as a lower bound of validFrom for the next topology transaction.
|
||||
val lastSequencerTs =
|
||||
allTransactions
|
||||
.map(_.context.validFrom)
|
||||
.maxOption
|
||||
.getOrElse(throw new NoSuchElementException("Missing domain parameters!"))
|
||||
|
||||
logger.debug(s"Last sequencer timestamp is $lastSequencerTs.")
|
||||
|
||||
// Determine how long we need to wait until all incompatible domainParameters have become
|
||||
// invalid for at least minMediatorDeduplicationTimeout.
|
||||
val waitDuration = allTransactions
|
||||
.filterNot(
|
||||
_.item.compatibleWithNewLedgerTimeRecordTimeTolerance(newLedgerTimeRecordTimeTolerance)
|
||||
)
|
||||
.map { tx =>
|
||||
val elapsedForAtLeast = tx.context.validUntil match {
|
||||
case Some(validUntil) => Duration.between(validUntil, lastSequencerTs)
|
||||
case None => Duration.ZERO
|
||||
}
|
||||
minMediatorDeduplicationTimeout.asJava minus elapsedForAtLeast
|
||||
}
|
||||
.maxOption
|
||||
.getOrElse(Duration.ZERO)
|
||||
|
||||
if (waitDuration > Duration.ZERO) {
|
||||
logger.info(
|
||||
show"Found incompatible past domain parameters. Waiting for $waitDuration..."
|
||||
)
|
||||
|
||||
// Use the clock instead of Threading.sleep to support sim clock based tests.
|
||||
val delayF = consoleEnvironment.environment.clock
|
||||
.scheduleAt(
|
||||
_ => (),
|
||||
startTs.plus(waitDuration),
|
||||
) // avoid scheduleAfter, because that causes a race condition in integration tests
|
||||
.onShutdown(
|
||||
throw new IllegalStateException(
|
||||
"Update of ledgerTimeRecordTimeTolerance interrupted due to shutdown."
|
||||
)
|
||||
)
|
||||
// Do not submit checkPreconditions() to the clock because it is blocking and would therefore block the clock.
|
||||
delayF.flatMap(_ => checkPreconditions())
|
||||
} else {
|
||||
Future.unit
|
||||
}
|
||||
}
|
||||
|
||||
timeouts.unbounded.await("Wait until ledgerTimeRecordTimeTolerance can be increased.")(
|
||||
checkPreconditions()
|
||||
)
|
||||
|
||||
// Now that past values of mediatorDeduplicationTimeout have been large enough,
|
||||
// we can change ledgerTimeRecordTimeTolerance.
|
||||
logger.info(
|
||||
s"Now changing ledgerTimeRecordTimeTolerance to $newLedgerTimeRecordTimeTolerance..."
|
||||
)
|
||||
update_dynamic_domain_parameters(
|
||||
_.copy(ledgerTimeRecordTimeTolerance = newLedgerTimeRecordTimeTolerance),
|
||||
force = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import better.files.File
|
||||
import com.digitalasset.canton.DiscardOps
|
||||
import com.digitalasset.canton.util.ResourceUtil
|
||||
import com.google.protobuf.ByteString
|
||||
import io.grpc.stub.StreamObserver
|
||||
|
||||
import java.io.FileOutputStream
|
||||
import scala.concurrent.Promise
|
||||
import scala.language.reflectiveCalls
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
private[commands] class GrpcByteChunksToFileObserver[
|
||||
T <: GrpcByteChunksToFileObserver.ByteStringChunk
|
||||
](
|
||||
inputFile: File,
|
||||
requestComplete: Promise[String],
|
||||
) extends StreamObserver[T] {
|
||||
private val os: FileOutputStream = inputFile.newFileOutputStream(append = false)
|
||||
|
||||
override def onNext(value: T): Unit = {
|
||||
Try(os.write(value.chunk.toByteArray)) match {
|
||||
case Failure(exception) =>
|
||||
ResourceUtil.closeAndAddSuppressed(Some(exception), os)
|
||||
throw exception
|
||||
case Success(_) => // all good
|
||||
}
|
||||
}
|
||||
|
||||
override def onError(t: Throwable): Unit = {
|
||||
requestComplete.tryFailure(t).discard
|
||||
ResourceUtil.closeAndAddSuppressed(None, os)
|
||||
}
|
||||
|
||||
override def onCompleted(): Unit = {
|
||||
requestComplete.trySuccess(inputFile.pathAsString).discard
|
||||
ResourceUtil.closeAndAddSuppressed(None, os)
|
||||
}
|
||||
}
|
||||
|
||||
private[commands] object GrpcByteChunksToFileObserver {
|
||||
type ByteStringChunk = { val chunk: ByteString }
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import better.files.File
|
||||
import com.digitalasset.canton.admin.api.client.commands.{
|
||||
StatusAdminCommands,
|
||||
TopologyAdminCommands,
|
||||
TopologyAdminCommandsX,
|
||||
}
|
||||
import com.digitalasset.canton.config.{ConsoleCommandTimeout, NonNegativeDuration}
|
||||
import com.digitalasset.canton.console.CommandErrors.{CommandError, GenericCommandError}
|
||||
import com.digitalasset.canton.console.ConsoleMacros.utils
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
CantonHealthAdministration,
|
||||
CommandErrors,
|
||||
CommandSuccessful,
|
||||
ConsoleCommandResult,
|
||||
ConsoleEnvironment,
|
||||
Help,
|
||||
Helpful,
|
||||
}
|
||||
import com.digitalasset.canton.health.admin.data.NodeStatus
|
||||
import com.digitalasset.canton.health.admin.v0.HealthDumpChunk
|
||||
import com.digitalasset.canton.health.admin.{data, v0}
|
||||
import com.digitalasset.canton.networking.grpc.GrpcError
|
||||
import com.digitalasset.canton.serialization.ProtoConverter.ParsingResult
|
||||
import com.digitalasset.canton.util.ResourceUtil
|
||||
import io.grpc.StatusRuntimeException
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import scala.concurrent.{Await, Promise, TimeoutException}
|
||||
|
||||
abstract class HealthAdministrationCommon[S <: data.NodeStatus.Status](
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
deserialize: v0.NodeStatus.Status => ParsingResult[S],
|
||||
) extends Helpful {
|
||||
private val initializedCache = new AtomicReference[Boolean](false)
|
||||
private def timeouts: ConsoleCommandTimeout = consoleEnvironment.commandTimeouts
|
||||
|
||||
import runner.*
|
||||
|
||||
@Help.Summary("Get human (and machine) readable status info")
|
||||
def status: data.NodeStatus[S] = consoleEnvironment.run {
|
||||
CommandSuccessful(adminCommand(new StatusAdminCommands.GetStatus[S](deserialize)) match {
|
||||
case CommandSuccessful(success) => success
|
||||
case err: CommandError => data.NodeStatus.Failure(err.cause)
|
||||
})
|
||||
}
|
||||
|
||||
@Help.Summary("Returns true if the node has an identity")
|
||||
def has_identity(): Boolean
|
||||
|
||||
@Help.Summary("Wait for the node to have an identity")
|
||||
@Help.Description(
|
||||
"""This is specifically useful for the Domain Manager which needs its identity to be ready for bootstrapping,
|
||||
| but for which we can't rely on wait_for_initialized() because it will be initialized only after being bootstrapped."""
|
||||
)
|
||||
def wait_for_identity(): Unit = waitFor(has_identity())
|
||||
|
||||
@Help.Summary(
|
||||
"Creates a zip file containing diagnostic information about the canton process running this node"
|
||||
)
|
||||
def dump(
|
||||
outputFile: File = CantonHealthAdministration.defaultHealthDumpName,
|
||||
timeout: NonNegativeDuration = timeouts.ledgerCommand,
|
||||
chunkSize: Option[Int] = None,
|
||||
): String = consoleEnvironment.run {
|
||||
val requestComplete = Promise[String]()
|
||||
val responseObserver =
|
||||
new GrpcByteChunksToFileObserver[HealthDumpChunk](outputFile, requestComplete)
|
||||
|
||||
def call = consoleEnvironment.run {
|
||||
adminCommand(new StatusAdminCommands.GetHealthDump(responseObserver, chunkSize))
|
||||
}
|
||||
|
||||
try {
|
||||
ResourceUtil.withResource(call) { _ =>
|
||||
CommandSuccessful(
|
||||
Await.result(requestComplete.future, timeout.duration)
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
case sre: StatusRuntimeException =>
|
||||
GenericCommandError(GrpcError("Generating health dump file", "dump", sre).toString)
|
||||
case _: TimeoutException =>
|
||||
outputFile.delete(swallowIOExceptions = true)
|
||||
CommandErrors.ConsoleTimeout.Error(timeout.asJavaApproximation)
|
||||
}
|
||||
}
|
||||
|
||||
private def runningCommand =
|
||||
adminCommand(
|
||||
StatusAdminCommands.IsRunning
|
||||
)
|
||||
private def initializedCommand =
|
||||
adminCommand(
|
||||
StatusAdminCommands.IsInitialized
|
||||
)
|
||||
|
||||
def falseIfUnreachable(command: ConsoleCommandResult[Boolean]): Boolean =
|
||||
consoleEnvironment.run(CommandSuccessful(command match {
|
||||
case CommandSuccessful(result) => result
|
||||
case _: CommandError => false
|
||||
}))
|
||||
|
||||
@Help.Summary("Check if the node is running")
|
||||
def running(): Boolean =
|
||||
// in case the node is not reachable, we assume it is not running
|
||||
falseIfUnreachable(runningCommand)
|
||||
|
||||
@Help.Summary("Check if the node is running and is the active instance (mediator, participant)")
|
||||
def active: Boolean = status match {
|
||||
case NodeStatus.Success(status) => status.active
|
||||
case NodeStatus.NotInitialized(active) => active
|
||||
case _ => false
|
||||
}
|
||||
|
||||
@Help.Summary("Returns true if node has been initialized.")
|
||||
def initialized(): Boolean = initializedCache.updateAndGet {
|
||||
case false =>
|
||||
// in case the node is not reachable, we cannot assume it is not initialized, because it could have been initialized in the past
|
||||
// and it's simply not running at the moment. so we'll allow the command to throw an error here
|
||||
consoleEnvironment.run(initializedCommand)
|
||||
case x => x
|
||||
}
|
||||
|
||||
@Help.Summary("Wait for the node to be running")
|
||||
def wait_for_running(): Unit = waitFor(running())
|
||||
|
||||
@Help.Summary("Wait for the node to be initialized")
|
||||
def wait_for_initialized(): Unit = {
|
||||
waitFor(initializedCache.updateAndGet {
|
||||
case false =>
|
||||
// in case the node is not reachable, we return false instead of throwing an error in order to keep retrying
|
||||
falseIfUnreachable(initializedCommand)
|
||||
case x => x
|
||||
})
|
||||
}
|
||||
|
||||
protected def waitFor(condition: => Boolean): Unit = {
|
||||
// all calls here are potentially unbounded. we do not know how long it takes
|
||||
// for a node to start or for a node to become initialised. so we use the unbounded
|
||||
// timeout
|
||||
utils.retry_until_true(timeout = consoleEnvironment.commandTimeouts.unbounded)(condition)
|
||||
}
|
||||
}
|
||||
|
||||
class HealthAdministration[S <: data.NodeStatus.Status](
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
deserialize: v0.NodeStatus.Status => ParsingResult[S],
|
||||
) extends HealthAdministrationCommon[S](runner, consoleEnvironment, deserialize) {
|
||||
|
||||
override def has_identity(): Boolean = runner
|
||||
.adminCommand(
|
||||
TopologyAdminCommands.Init.GetId()
|
||||
)
|
||||
.toEither
|
||||
.isRight
|
||||
|
||||
}
|
||||
|
||||
class HealthAdministrationX[S <: data.NodeStatus.Status](
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
deserialize: v0.NodeStatus.Status => ParsingResult[S],
|
||||
) extends HealthAdministrationCommon[S](runner, consoleEnvironment, deserialize) {
|
||||
|
||||
override def has_identity(): Boolean = runner
|
||||
.adminCommand(
|
||||
TopologyAdminCommandsX.Init.GetId()
|
||||
)
|
||||
.toEither
|
||||
.isRight
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.commands.EnterpriseMediatorAdministrationCommands.{
|
||||
Initialize,
|
||||
InitializeX,
|
||||
LocatePruningTimestampCommand,
|
||||
Prune,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.commands.{
|
||||
DomainTimeCommands,
|
||||
PruningSchedulerCommands,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.data.StaticDomainParameters
|
||||
import com.digitalasset.canton.config.NonNegativeDuration
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
ConsoleEnvironment,
|
||||
FeatureFlag,
|
||||
FeatureFlagFilter,
|
||||
Help,
|
||||
Helpful,
|
||||
}
|
||||
import com.digitalasset.canton.crypto.{Fingerprint, PublicKey}
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.domain.admin.v0.EnterpriseMediatorAdministrationServiceGrpc
|
||||
import com.digitalasset.canton.domain.admin.v0.EnterpriseMediatorAdministrationServiceGrpc.EnterpriseMediatorAdministrationServiceStub
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.sequencing.{SequencerConnection, SequencerConnections}
|
||||
import com.digitalasset.canton.time.NonNegativeFiniteDuration
|
||||
import com.digitalasset.canton.topology.store.StoredTopologyTransactions
|
||||
import com.digitalasset.canton.topology.transaction.TopologyChangeOp
|
||||
import com.digitalasset.canton.topology.{DomainId, MediatorId}
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
class MediatorTestingGroup(
|
||||
runner: AdminCommandRunner,
|
||||
val consoleEnvironment: ConsoleEnvironment,
|
||||
val loggerFactory: NamedLoggerFactory,
|
||||
) extends FeatureFlagFilter
|
||||
with Helpful {
|
||||
|
||||
@Help.Summary("Fetch the current time from the domain", FeatureFlag.Testing)
|
||||
def fetch_domain_time(
|
||||
timeout: NonNegativeDuration = consoleEnvironment.commandTimeouts.ledgerCommand
|
||||
): CantonTimestamp =
|
||||
check(FeatureFlag.Testing) {
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
DomainTimeCommands.FetchTime(None, NonNegativeFiniteDuration.Zero, timeout)
|
||||
)
|
||||
}.timestamp
|
||||
}
|
||||
|
||||
@Help.Summary("Await for the given time to be reached on the domain", FeatureFlag.Testing)
|
||||
def await_domain_time(time: CantonTimestamp, timeout: NonNegativeDuration): Unit =
|
||||
check(FeatureFlag.Testing) {
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
DomainTimeCommands.AwaitTime(None, time, timeout)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MediatorPruningAdministrationGroup(
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
) extends PruningSchedulerAdministration(
|
||||
runner,
|
||||
consoleEnvironment,
|
||||
new PruningSchedulerCommands[EnterpriseMediatorAdministrationServiceStub](
|
||||
EnterpriseMediatorAdministrationServiceGrpc.stub,
|
||||
_.setSchedule(_),
|
||||
_.clearSchedule(_),
|
||||
_.setCron(_),
|
||||
_.setMaxDuration(_),
|
||||
_.setRetention(_),
|
||||
_.getSchedule(_),
|
||||
),
|
||||
loggerFactory,
|
||||
)
|
||||
with Helpful {
|
||||
|
||||
@Help.Summary(
|
||||
"Prune the mediator of unnecessary data while keeping data for the default retention period"
|
||||
)
|
||||
@Help.Description(
|
||||
"""Removes unnecessary data from the Mediator that is earlier than the default retention period.
|
||||
|The default retention period is set in the configuration of the canton node running this
|
||||
|command under `parameters.retention-period-defaults.mediator`."""
|
||||
)
|
||||
def prune(): Unit = {
|
||||
val defaultRetention =
|
||||
consoleEnvironment.environment.config.parameters.retentionPeriodDefaults.mediator
|
||||
prune_with_retention_period(defaultRetention.underlying)
|
||||
}
|
||||
|
||||
@Help.Summary(
|
||||
"Prune the mediator of unnecessary data while keeping data for the provided retention period"
|
||||
)
|
||||
def prune_with_retention_period(retentionPeriod: FiniteDuration): Unit = {
|
||||
import scala.jdk.DurationConverters.*
|
||||
val pruneUpTo = consoleEnvironment.environment.clock.now.minus(retentionPeriod.toJava)
|
||||
prune_at(pruneUpTo)
|
||||
}
|
||||
|
||||
@Help.Summary("Prune the mediator of unnecessary data up to and including the given timestamp")
|
||||
def prune_at(timestamp: CantonTimestamp): Unit = consoleEnvironment.run {
|
||||
runner.adminCommand(Prune(timestamp))
|
||||
}
|
||||
|
||||
@Help.Summary("Obtain a timestamp at or near the beginning of mediator state")
|
||||
@Help.Description(
|
||||
"""This command provides insight into the current state of mediator pruning when called with
|
||||
|the default value of `index` 1.
|
||||
|When pruning the mediator manually via `prune_at` and with the intent to prune in batches, specify
|
||||
|a value such as 1000 to obtain a pruning timestamp that corresponds to the "end" of the batch."""
|
||||
)
|
||||
def locate_pruning_timestamp(
|
||||
index: PositiveInt = PositiveInt.tryCreate(1)
|
||||
): Option[CantonTimestamp] =
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(LocatePruningTimestampCommand(index))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MediatorAdministrationGroup(
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
) extends MediatorPruningAdministrationGroup(runner, consoleEnvironment, loggerFactory) {
|
||||
|
||||
private lazy val testing_ = new MediatorTestingGroup(runner, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Testing functionality for the mediator")
|
||||
@Help.Group("Testing")
|
||||
def testing: MediatorTestingGroup = testing_
|
||||
|
||||
}
|
||||
|
||||
@Help.Summary("Manage the mediator component")
|
||||
@Help.Group("Mediator")
|
||||
class MediatorAdministrationGroupWithInit(
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
) extends MediatorAdministrationGroup(runner, consoleEnvironment, loggerFactory) {
|
||||
|
||||
@Help.Summary("Initialize a mediator")
|
||||
def initialize(
|
||||
domainId: DomainId,
|
||||
mediatorId: MediatorId,
|
||||
domainParameters: StaticDomainParameters,
|
||||
sequencerConnections: SequencerConnections,
|
||||
topologySnapshot: Option[StoredTopologyTransactions[TopologyChangeOp.Positive]],
|
||||
signingKeyFingerprint: Option[Fingerprint] = None,
|
||||
): PublicKey = consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
Initialize(
|
||||
domainId,
|
||||
mediatorId,
|
||||
topologySnapshot,
|
||||
domainParameters.toInternal,
|
||||
sequencerConnections,
|
||||
signingKeyFingerprint,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Initialize a mediator")
|
||||
def initialize(
|
||||
domainId: DomainId,
|
||||
mediatorId: MediatorId,
|
||||
domainParameters: StaticDomainParameters,
|
||||
sequencerConnection: SequencerConnection,
|
||||
topologySnapshot: Option[StoredTopologyTransactions[TopologyChangeOp.Positive]],
|
||||
signingKeyFingerprint: Option[Fingerprint],
|
||||
): PublicKey = consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
Initialize(
|
||||
domainId,
|
||||
mediatorId,
|
||||
topologySnapshot,
|
||||
domainParameters.toInternal,
|
||||
SequencerConnections.single(sequencerConnection),
|
||||
signingKeyFingerprint,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait MediatorXAdministrationGroupWithInit extends ConsoleCommandGroup {
|
||||
|
||||
@Help.Summary("Methods used to initialize the node")
|
||||
object setup extends ConsoleCommandGroup.Impl(this) with InitNodeId {
|
||||
|
||||
@Help.Summary("Assign a mediator to a domain")
|
||||
def assign(
|
||||
domainId: DomainId,
|
||||
domainParameters: StaticDomainParameters,
|
||||
sequencerConnections: SequencerConnections,
|
||||
): Unit = consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
InitializeX(
|
||||
domainId,
|
||||
domainParameters.toInternal,
|
||||
sequencerConnections,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private lazy val testing_ = new MediatorTestingGroup(runner, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Testing functionality for the mediator")
|
||||
@Help.Group("Testing")
|
||||
def testing: MediatorTestingGroup = testing_
|
||||
|
||||
private lazy val pruning_ =
|
||||
new MediatorPruningAdministrationGroup(runner, consoleEnvironment, loggerFactory)
|
||||
@Help.Summary("Pruning functionality for the mediator")
|
||||
@Help.Group("Testing")
|
||||
def pruning: MediatorPruningAdministrationGroup = pruning_
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,368 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import better.files.File
|
||||
import com.digitalasset.canton.admin.api.client.commands.{
|
||||
GrpcAdminCommand,
|
||||
ParticipantAdminCommands,
|
||||
}
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.console.CommandErrors.GenericCommandError
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
CommandErrors,
|
||||
CommandSuccessful,
|
||||
ConsoleCommandResult,
|
||||
ConsoleEnvironment,
|
||||
FeatureFlag,
|
||||
FeatureFlagFilter,
|
||||
Help,
|
||||
Helpful,
|
||||
}
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.networking.grpc.GrpcError
|
||||
import com.digitalasset.canton.participant.ParticipantNodeCommon
|
||||
import com.digitalasset.canton.participant.admin.v0.{ExportAcsRequest, ExportAcsResponse}
|
||||
import com.digitalasset.canton.participant.domain.DomainConnectionConfig
|
||||
import com.digitalasset.canton.protocol.{LfContractId, SerializableContractWithWitnesses}
|
||||
import com.digitalasset.canton.topology.{DomainId, PartyId}
|
||||
import com.digitalasset.canton.tracing.{NoTracing, TraceContext}
|
||||
import com.digitalasset.canton.util.ResourceUtil
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
import com.digitalasset.canton.{DiscardOps, DomainAlias, SequencerCounter}
|
||||
import com.google.protobuf.ByteString
|
||||
import io.grpc.Context.CancellableContext
|
||||
import io.grpc.StatusRuntimeException
|
||||
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import scala.concurrent.{Await, Promise, TimeoutException}
|
||||
|
||||
class ParticipantRepairAdministration(
|
||||
val consoleEnvironment: ConsoleEnvironment,
|
||||
runner: AdminCommandRunner,
|
||||
val loggerFactory: NamedLoggerFactory,
|
||||
) extends FeatureFlagFilter
|
||||
with NoTracing
|
||||
with Helpful {
|
||||
|
||||
@Help.Summary("Purge contracts with specified Contract IDs from local participant.")
|
||||
@Help.Description(
|
||||
"""This is a last resort command to recover from data corruption, e.g. in scenarios in which participant
|
||||
|contracts have somehow gotten out of sync and need to be manually purged, or in situations in which
|
||||
|stakeholders are no longer available to agree to their archival. The participant needs to be disconnected from
|
||||
|the domain on which the contracts with "contractIds" reside at the time of the call, and as of now the domain
|
||||
|cannot have had any inflight requests.
|
||||
|The "ignoreAlreadyPurged" flag makes it possible to invoke the command multiple times with the same
|
||||
|parameters in case an earlier command invocation has failed.
|
||||
|As repair commands are powerful tools to recover from unforeseen data corruption, but dangerous under normal
|
||||
|operation, use of this command requires (temporarily) enabling the "features.enable-repair-commands"
|
||||
|configuration. In addition repair commands can run for an unbounded time depending on the number of
|
||||
|contract ids passed in. Be sure to not connect the participant to the domain until the call returns."""
|
||||
)
|
||||
def purge(
|
||||
domain: DomainAlias,
|
||||
contractIds: Seq[LfContractId],
|
||||
ignoreAlreadyPurged: Boolean = true,
|
||||
): Unit =
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
ParticipantAdminCommands.ParticipantRepairManagement.PurgeContracts(
|
||||
domain = domain,
|
||||
contracts = contractIds,
|
||||
ignoreAlreadyPurged = ignoreAlreadyPurged,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Migrate contracts from one domain to another one.")
|
||||
@Help.Description(
|
||||
"""This method can be used to migrate all the contracts associated with a domain to a new domain connection.
|
||||
This method will register the new domain, connect to it and then re-associate all contracts on the source
|
||||
domain to the target domain. Please note that this migration needs to be done by all participants
|
||||
at the same time. The domain should only be used once all participants have finished their migration.
|
||||
|
||||
The arguments are:
|
||||
source: the domain alias of the source domain
|
||||
target: the configuration for the target domain
|
||||
"""
|
||||
)
|
||||
def migrate_domain(
|
||||
source: DomainAlias,
|
||||
target: DomainConnectionConfig,
|
||||
): Unit = {
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
ParticipantAdminCommands.ParticipantRepairManagement.MigrateDomain(source, target)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Export active contracts for the given set of parties to a file.")
|
||||
@Help.Description(
|
||||
"""This command exports the current Active Contract Set (ACS) of a given set of parties to ACS snapshot file.
|
||||
|Afterwards, the 'import_acs' command allows importing it into a participant's ACS again.
|
||||
|Such ACS export (and import) is interesting for recovery and operational purposes only.
|
||||
|Note that the 'export_acs' command execution may take a long time to complete and may require significant
|
||||
|resources.
|
||||
"""
|
||||
)
|
||||
def export_acs(
|
||||
parties: Set[PartyId],
|
||||
outputFile: String = ParticipantRepairAdministration.ExportAcsDefaultFile,
|
||||
filterDomainId: Option[DomainId] = None,
|
||||
timestamp: Option[Instant] = None,
|
||||
contractDomainRenames: Map[DomainId, (DomainId, ProtocolVersion)] = Map.empty,
|
||||
): Unit = {
|
||||
check(FeatureFlag.Repair) {
|
||||
val collector = AcsSnapshotFileCollector[ExportAcsRequest, ExportAcsResponse](outputFile)
|
||||
val command = ParticipantAdminCommands.ParticipantRepairManagement
|
||||
.ExportAcs(
|
||||
parties,
|
||||
filterDomainId,
|
||||
timestamp,
|
||||
collector.observer,
|
||||
contractDomainRenames,
|
||||
)
|
||||
collector.materializeFile(command)
|
||||
}
|
||||
}
|
||||
|
||||
private case class AcsSnapshotFileCollector[
|
||||
Req,
|
||||
Resp <: GrpcByteChunksToFileObserver.ByteStringChunk,
|
||||
](outputFile: String) {
|
||||
private val target = File(outputFile)
|
||||
private val requestComplete = Promise[String]()
|
||||
val observer = new GrpcByteChunksToFileObserver[Resp](
|
||||
target,
|
||||
requestComplete,
|
||||
)
|
||||
private val timeout = consoleEnvironment.commandTimeouts.ledgerCommand
|
||||
|
||||
def materializeFile(
|
||||
command: GrpcAdminCommand[
|
||||
Req,
|
||||
CancellableContext,
|
||||
CancellableContext,
|
||||
]
|
||||
): Unit = {
|
||||
consoleEnvironment.run {
|
||||
|
||||
def call = consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
command
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
ResourceUtil.withResource(call) { _ =>
|
||||
CommandSuccessful(
|
||||
Await
|
||||
.result(
|
||||
requestComplete.future,
|
||||
timeout.duration,
|
||||
)
|
||||
.discard
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
case sre: StatusRuntimeException =>
|
||||
GenericCommandError(
|
||||
GrpcError("Generating acs snapshot file", "download_acs_snapshot", sre).toString
|
||||
)
|
||||
case _: TimeoutException =>
|
||||
target.delete(swallowIOExceptions = true)
|
||||
CommandErrors.ConsoleTimeout.Error(timeout.asJavaApproximation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Import active contracts from an Active Contract Set (ACS) snapshot file.")
|
||||
@Help.Description(
|
||||
"""This command imports contracts from an ACS snapshot file into the participant's ACS.
|
||||
|The given ACS snapshot file needs to be the resulting file from a previous 'export_acs' command invocation.
|
||||
"""
|
||||
)
|
||||
def import_acs(
|
||||
inputFile: String = ParticipantRepairAdministration.ExportAcsDefaultFile,
|
||||
workflowIdPrefix: String = "",
|
||||
): Unit = {
|
||||
check(FeatureFlag.Repair) {
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(
|
||||
ParticipantAdminCommands.ParticipantRepairManagement.ImportAcs(
|
||||
ByteString.copyFrom(File(inputFile).loadBytes),
|
||||
if (workflowIdPrefix.nonEmpty) workflowIdPrefix
|
||||
else s"import-${UUID.randomUUID}",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class LocalParticipantRepairAdministration(
|
||||
override val consoleEnvironment: ConsoleEnvironment,
|
||||
runner: AdminCommandRunner,
|
||||
override val loggerFactory: NamedLoggerFactory,
|
||||
) extends ParticipantRepairAdministration(
|
||||
consoleEnvironment = consoleEnvironment,
|
||||
runner = runner,
|
||||
loggerFactory = loggerFactory,
|
||||
) {
|
||||
|
||||
protected def access[T](handler: ParticipantNodeCommon => T): T
|
||||
|
||||
@Help.Summary("Add specified contracts to specific domain on local participant.")
|
||||
@Help.Description(
|
||||
"""This is a last resort command to recover from data corruption, e.g. in scenarios in which participant
|
||||
|contracts have somehow gotten out of sync and need to be manually created. The participant needs to be
|
||||
|disconnected from the specified "domain" at the time of the call, and as of now the domain cannot have had
|
||||
|any inflight requests.
|
||||
|For each "contractsToAdd", specify "witnesses", local parties, in case no local party is a stakeholder.
|
||||
|The "ignoreAlreadyAdded" flag makes it possible to invoke the command multiple times with the same
|
||||
|parameters in case an earlier command invocation has failed.
|
||||
|
|
||||
|As repair commands are powerful tools to recover from unforeseen data corruption, but dangerous under normal
|
||||
|operation, use of this command requires (temporarily) enabling the "features.enable-repair-commands"
|
||||
|configuration. In addition repair commands can run for an unbounded time depending on the number of
|
||||
|contracts passed in. Be sure to not connect the participant to the domain until the call returns.
|
||||
|
|
||||
The arguments are:
|
||||
- domain: the alias of the domain to which to add the contract
|
||||
- contractsToAdd: list of contracts to add with witness information
|
||||
- ignoreAlreadyAdded: (default true) if set to true, it will ignore contracts that already exist on the target domain.
|
||||
- ignoreStakeholderCheck: (default false) if set to true, add will work for contracts that don't have a local party (useful for party migration).
|
||||
"""
|
||||
)
|
||||
def add(
|
||||
domain: DomainAlias,
|
||||
contractsToAdd: Seq[SerializableContractWithWitnesses],
|
||||
ignoreAlreadyAdded: Boolean = true,
|
||||
ignoreStakeholderCheck: Boolean = false,
|
||||
): Unit =
|
||||
runRepairCommand(tc =>
|
||||
access(
|
||||
_.sync.repairService
|
||||
.addContracts(
|
||||
domain,
|
||||
contractsToAdd,
|
||||
ignoreAlreadyAdded,
|
||||
ignoreStakeholderCheck,
|
||||
)(tc)
|
||||
)
|
||||
)
|
||||
|
||||
private def runRepairCommand[T](command: TraceContext => Either[String, T]): T =
|
||||
check(FeatureFlag.Repair) {
|
||||
consoleEnvironment.run {
|
||||
ConsoleCommandResult.fromEither {
|
||||
// Ensure that admin repair commands have a non-empty trace context.
|
||||
TraceContext.withNewTraceContext(command(_))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Move contracts with specified Contract IDs from one domain to another.")
|
||||
@Help.Description(
|
||||
"""This is a last resort command to recover from data corruption in scenarios in which a domain is
|
||||
|irreparably broken and formerly connected participants need to move contracts to another, healthy domain.
|
||||
|The participant needs to be disconnected from both the "sourceDomain" and the "targetDomain". Also as of now
|
||||
|the target domain cannot have had any inflight requests.
|
||||
|Contracts already present in the target domain will be skipped, and this makes it possible to invoke this
|
||||
|command in an "idempotent" fashion in case an earlier attempt had resulted in an error.
|
||||
|The "skipInactive" flag makes it possible to only move active contracts in the "sourceDomain".
|
||||
|As repair commands are powerful tools to recover from unforeseen data corruption, but dangerous under normal
|
||||
|operation, use of this command requires (temporarily) enabling the "features.enable-repair-commands"
|
||||
|configuration. In addition repair commands can run for an unbounded time depending on the number of
|
||||
|contract ids passed in. Be sure to not connect the participant to either domain until the call returns.
|
||||
|
||||
Arguments:
|
||||
- contractIds - set of contract ids that should be moved to the new domain
|
||||
- sourceDomain - alias of the source domain
|
||||
- targetDomain - alias of the target domain
|
||||
- skipInactive - (default true) whether to skip inactive contracts mentioned in the contractIds list
|
||||
- batchSize - (default 100) how many contracts to write at once to the database"""
|
||||
)
|
||||
def change_domain(
|
||||
contractIds: Seq[LfContractId],
|
||||
sourceDomain: DomainAlias,
|
||||
targetDomain: DomainAlias,
|
||||
skipInactive: Boolean = true,
|
||||
batchSize: Int = 100,
|
||||
): Unit =
|
||||
runRepairCommand(tc =>
|
||||
access(
|
||||
_.sync.repairService.changeDomainAwait(
|
||||
contractIds,
|
||||
sourceDomain,
|
||||
targetDomain,
|
||||
skipInactive,
|
||||
PositiveInt.tryCreate(batchSize),
|
||||
)(tc)
|
||||
)
|
||||
)
|
||||
|
||||
@Help.Summary("Mark sequenced events as ignored.")
|
||||
@Help.Description(
|
||||
"""This is the last resort to ignore events that the participant is unable to process.
|
||||
|Ignoring events may lead to subsequent failures, e.g., if the event creating a contract is ignored and
|
||||
|that contract is subsequently used. It may also lead to ledger forks if other participants still process
|
||||
|the ignored events.
|
||||
|It is possible to mark events as ignored that the participant has not yet received.
|
||||
|
|
||||
|The command will fail, if marking events between `from` and `to` as ignored would result in a gap in sequencer counters,
|
||||
|namely if `from <= to` and `from` is greater than `maxSequencerCounter + 1`,
|
||||
|where `maxSequencerCounter` is the greatest sequencer counter of a sequenced event stored by the underlying participant.
|
||||
|
|
||||
|The command will also fail, if `force == false` and `from` is smaller than the sequencer counter of the last event
|
||||
|that has been marked as clean.
|
||||
|(Ignoring such events would normally have no effect, as they have already been processed.)"""
|
||||
)
|
||||
def ignore_events(
|
||||
domainId: DomainId,
|
||||
from: SequencerCounter,
|
||||
to: SequencerCounter,
|
||||
force: Boolean = false,
|
||||
): Unit =
|
||||
runRepairCommand(tc =>
|
||||
access {
|
||||
_.sync.repairService.ignoreEvents(domainId, from, to, force)(tc)
|
||||
}
|
||||
)
|
||||
|
||||
@Help.Summary("Remove the ignored status from sequenced events.")
|
||||
@Help.Description(
|
||||
"""This command has no effect on ordinary (i.e., not ignored) events and on events that do not exist.
|
||||
|
|
||||
|The command will fail, if marking events between `from` and `to` as unignored would result in a gap in sequencer counters,
|
||||
|namely if there is one empty ignored event with sequencer counter between `from` and `to` and
|
||||
|another empty ignored event with sequencer counter greater than `to`.
|
||||
|An empty ignored event is an event that has been marked as ignored and not yet received by the participant.
|
||||
|
|
||||
|The command will also fail, if `force == false` and `from` is smaller than the sequencer counter of the last event
|
||||
|that has been marked as clean.
|
||||
|(Unignoring such events would normally have no effect, as they have already been processed.)"""
|
||||
)
|
||||
def unignore_events(
|
||||
domainId: DomainId,
|
||||
from: SequencerCounter,
|
||||
to: SequencerCounter,
|
||||
force: Boolean = false,
|
||||
): Unit =
|
||||
runRepairCommand(tc =>
|
||||
access {
|
||||
_.sync.repairService.unignoreEvents(domainId, from, to, force)(tc)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
object ParticipantRepairAdministration {
|
||||
private val DefaultFile = "canton-acs-snapshot.gz"
|
||||
private val ExportAcsDefaultFile = "canton-acs-export.gz"
|
||||
}
|
@ -0,0 +1,398 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.foldable.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.digitalasset.canton.LedgerParticipantId
|
||||
import com.digitalasset.canton.admin.api.client.commands.{
|
||||
ParticipantAdminCommands,
|
||||
TopologyAdminCommands,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.data.{
|
||||
ListConnectedDomainsResult,
|
||||
ListPartiesResult,
|
||||
PartyDetails,
|
||||
}
|
||||
import com.digitalasset.canton.config.CantonRequireTypes.String255
|
||||
import com.digitalasset.canton.config.NonNegativeDuration
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
BaseInspection,
|
||||
CantonInternalError,
|
||||
CommandFailure,
|
||||
ConsoleCommandResult,
|
||||
ConsoleEnvironment,
|
||||
ConsoleMacros,
|
||||
FeatureFlag,
|
||||
FeatureFlagFilter,
|
||||
Help,
|
||||
Helpful,
|
||||
LocalParticipantReference,
|
||||
ParticipantReference,
|
||||
}
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.participant.ParticipantNode
|
||||
import com.digitalasset.canton.topology.*
|
||||
import com.digitalasset.canton.topology.transaction.{
|
||||
ParticipantPermission,
|
||||
RequestSide,
|
||||
TopologyChangeOp,
|
||||
}
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.digitalasset.canton.util.ShowUtil.*
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
import java.time.Instant
|
||||
import scala.util.Try
|
||||
|
||||
class PartiesAdministrationGroup(runner: AdminCommandRunner, consoleEnvironment: ConsoleEnvironment)
|
||||
extends Helpful {
|
||||
|
||||
protected def defaultLimit: PositiveInt =
|
||||
consoleEnvironment.environment.config.parameters.console.defaultLimit
|
||||
|
||||
import runner.*
|
||||
|
||||
@Help.Summary(
|
||||
"List active parties, their active participants, and the participants' permissions on domains."
|
||||
)
|
||||
@Help.Description(
|
||||
"""Inspect the parties known by this participant as used for synchronisation.
|
||||
|The response is built from the timestamped topology transactions of each domain, excluding the
|
||||
|authorized store of the given node. For each known party, the list of active
|
||||
|participants and their permission on the domain for that party is given.
|
||||
|
|
||||
filterParty: Filter by parties starting with the given string.
|
||||
filterParticipant: Filter for parties that are hosted by a participant with an id starting with the given string
|
||||
filterDomain: Filter by domains whose id starts with the given string.
|
||||
asOf: Optional timestamp to inspect the topology state at a given point in time.
|
||||
limit: Limit on the number of parties fetched (defaults to canton.parameters.console.default-limit).
|
||||
|
||||
Example: participant1.parties.list(filterParty="alice")
|
||||
"""
|
||||
)
|
||||
def list(
|
||||
filterParty: String = "",
|
||||
filterParticipant: String = "",
|
||||
filterDomain: String = "",
|
||||
asOf: Option[Instant] = None,
|
||||
limit: PositiveInt = defaultLimit,
|
||||
): Seq[ListPartiesResult] =
|
||||
consoleEnvironment.run {
|
||||
adminCommand(
|
||||
TopologyAdminCommands.Aggregation.ListParties(
|
||||
filterDomain = filterDomain,
|
||||
filterParty = filterParty,
|
||||
filterParticipant = filterParticipant,
|
||||
asOf = asOf,
|
||||
limit = limit,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ParticipantPartiesAdministrationGroup(
|
||||
participantId: => ParticipantId,
|
||||
runner: AdminCommandRunner & ParticipantAdministration & BaseLedgerApiAdministration,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
) extends PartiesAdministrationGroup(runner, consoleEnvironment) {
|
||||
|
||||
@Help.Summary("List parties hosted by this participant")
|
||||
@Help.Description("""Inspect the parties hosted by this participant as used for synchronisation.
|
||||
|The response is built from the timestamped topology transactions of each domain, excluding the
|
||||
|authorized store of the given node. The search will include all hosted parties and is equivalent
|
||||
|to running the `list` method using the participant id of the invoking participant.
|
||||
|
|
||||
filterParty: Filter by parties starting with the given string.
|
||||
filterDomain: Filter by domains whose id starts with the given string.
|
||||
asOf: Optional timestamp to inspect the topology state at a given point in time.
|
||||
limit: How many items to return (defaults to canton.parameters.console.default-limit)
|
||||
|
||||
Example: participant1.parties.hosted(filterParty="alice")""")
|
||||
def hosted(
|
||||
filterParty: String = "",
|
||||
filterDomain: String = "",
|
||||
asOf: Option[Instant] = None,
|
||||
limit: PositiveInt = defaultLimit,
|
||||
): Seq[ListPartiesResult] = {
|
||||
list(
|
||||
filterParty,
|
||||
filterParticipant = participantId.filterString,
|
||||
filterDomain = filterDomain,
|
||||
asOf = asOf,
|
||||
limit = limit,
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Find a party from a filter string")
|
||||
@Help.Description(
|
||||
"""Will search for all parties that match this filter string. If it finds exactly one party, it
|
||||
|will return that one. Otherwise, the function will throw."""
|
||||
)
|
||||
def find(filterParty: String): PartyId = {
|
||||
list(filterParty).map(_.party).distinct.toList match {
|
||||
case one :: Nil => one
|
||||
case Nil => throw new IllegalArgumentException(s"No party matching $filterParty")
|
||||
case more =>
|
||||
throw new IllegalArgumentException(s"Multiple parties match $filterParty: $more")
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Enable/add party to participant")
|
||||
@Help.Description("""This function registers a new party with the current participant within the participants
|
||||
|namespace. The function fails if the participant does not have appropriate signing keys
|
||||
|to issue the corresponding PartyToParticipant topology transaction.
|
||||
|Optionally, a local display name can be added. This display name will be exposed on the
|
||||
|ledger API party management endpoint.
|
||||
|Specifying a set of domains via the `WaitForDomain` parameter ensures that the domains have
|
||||
|enabled/added a party by the time the call returns, but other participants connected to the same domains may not
|
||||
|yet be aware of the party.
|
||||
|Additionally, a sequence of additional participants can be added to be synchronized to
|
||||
|ensure that the party is known to these participants as well before the function terminates.
|
||||
|""")
|
||||
def enable(
|
||||
name: String,
|
||||
displayName: Option[String] = None,
|
||||
// TODO(i10809) replace wait for domain for a clean topology synchronisation using the dispatcher info
|
||||
waitForDomain: DomainChoice = DomainChoice.Only(Seq()),
|
||||
synchronizeParticipants: Seq[ParticipantReference] = Seq(),
|
||||
): PartyId = {
|
||||
|
||||
def registered(lst: => Seq[ListPartiesResult]): Set[DomainId] = {
|
||||
lst
|
||||
.flatMap(_.participants.flatMap(_.domains))
|
||||
.map(_.domain)
|
||||
.toSet
|
||||
}
|
||||
def primaryRegistered(partyId: PartyId) =
|
||||
registered(
|
||||
list(filterParty = partyId.filterString, filterParticipant = participantId.filterString)
|
||||
)
|
||||
|
||||
def primaryConnected: Either[String, Seq[ListConnectedDomainsResult]] =
|
||||
runner
|
||||
.adminCommand(ParticipantAdminCommands.DomainConnectivity.ListConnectedDomains())
|
||||
.toEither
|
||||
|
||||
def findDomainIds(
|
||||
name: String,
|
||||
connected: Either[String, Seq[ListConnectedDomainsResult]],
|
||||
): Either[String, Set[DomainId]] = {
|
||||
for {
|
||||
domainIds <- waitForDomain match {
|
||||
case DomainChoice.All =>
|
||||
connected.map(_.map(_.domainId))
|
||||
case DomainChoice.Only(Seq()) =>
|
||||
Right(Seq())
|
||||
case DomainChoice.Only(aliases) =>
|
||||
connected.flatMap { res =>
|
||||
val connectedM = res.map(x => (x.domainAlias, x.domainId)).toMap
|
||||
aliases.traverse(alias => connectedM.get(alias).toRight(s"Unknown: $alias for $name"))
|
||||
}
|
||||
}
|
||||
} yield domainIds.toSet
|
||||
}
|
||||
def retryE(condition: => Boolean, message: => String): Either[String, Unit] = {
|
||||
AdminCommandRunner
|
||||
.retryUntilTrue(consoleEnvironment.commandTimeouts.ledgerCommand)(condition)
|
||||
.toEither
|
||||
.leftMap(_ => message)
|
||||
}
|
||||
def waitForParty(
|
||||
partyId: PartyId,
|
||||
domainIds: Set[DomainId],
|
||||
registered: => Set[DomainId],
|
||||
queriedParticipant: ParticipantId = participantId,
|
||||
): Either[String, Unit] = {
|
||||
if (domainIds.nonEmpty) {
|
||||
retryE(
|
||||
domainIds subsetOf registered,
|
||||
show"Party $partyId did not appear for $queriedParticipant on domain ${domainIds.diff(registered)}",
|
||||
)
|
||||
} else Right(())
|
||||
}
|
||||
val syncLedgerApi = waitForDomain match {
|
||||
case DomainChoice.All => true
|
||||
case DomainChoice.Only(aliases) => aliases.nonEmpty
|
||||
}
|
||||
consoleEnvironment.run {
|
||||
ConsoleCommandResult.fromEither {
|
||||
for {
|
||||
// validating party and display name here to prevent, e.g., a party being registered despite it having an invalid display name
|
||||
// assert that name is valid ParticipantId
|
||||
id <- Identifier.create(name)
|
||||
partyId = PartyId(participantId.uid.copy(id = id))
|
||||
_ <- Either
|
||||
.catchOnly[IllegalArgumentException](LedgerParticipantId.assertFromString(name))
|
||||
.leftMap(_.getMessage)
|
||||
validDisplayName <- displayName.map(String255.create(_, Some("display name"))).sequence
|
||||
// find the domain ids
|
||||
domainIds <- findDomainIds(this.participantId.uid.id.unwrap, primaryConnected)
|
||||
// find the domain ids the additional participants are connected to
|
||||
additionalSync <- synchronizeParticipants.traverse { p =>
|
||||
findDomainIds(
|
||||
p.name,
|
||||
Try(p.domains.list_connected()).toEither.leftMap {
|
||||
case exception @ (_: CommandFailure | _: CantonInternalError) =>
|
||||
exception.getMessage
|
||||
case exception => throw exception
|
||||
},
|
||||
)
|
||||
.map(domains => (p, domains intersect domainIds))
|
||||
}
|
||||
_ <- runPartyCommand(partyId, TopologyChangeOp.Add).toEither
|
||||
_ <- validDisplayName match {
|
||||
case None => Right(())
|
||||
case Some(name) =>
|
||||
runner
|
||||
.adminCommand(
|
||||
ParticipantAdminCommands.PartyNameManagement
|
||||
.SetPartyDisplayName(partyId, name.unwrap)
|
||||
)
|
||||
.toEither
|
||||
}
|
||||
_ <- waitForParty(partyId, domainIds, primaryRegistered(partyId))
|
||||
_ <-
|
||||
// sync with ledger-api server if this node is connected to at least one domain
|
||||
if (syncLedgerApi && primaryConnected.exists(_.nonEmpty))
|
||||
retryE(
|
||||
runner.ledger_api.parties.list().map(_.party).contains(partyId),
|
||||
show"The party $partyId never appeared on the ledger API server",
|
||||
)
|
||||
else Right(())
|
||||
_ <- additionalSync.traverse_ { case (p, domains) =>
|
||||
waitForParty(
|
||||
partyId,
|
||||
domains,
|
||||
registered(
|
||||
p.parties.list(
|
||||
filterParty = partyId.filterString,
|
||||
filterParticipant = participantId.filterString,
|
||||
)
|
||||
),
|
||||
p.id,
|
||||
)
|
||||
}
|
||||
} yield partyId
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def runPartyCommand(
|
||||
partyId: PartyId,
|
||||
op: TopologyChangeOp,
|
||||
force: Boolean = false,
|
||||
): ConsoleCommandResult[ByteString] = {
|
||||
runner
|
||||
.adminCommand(
|
||||
TopologyAdminCommands.Write.AuthorizePartyToParticipant(
|
||||
op,
|
||||
None,
|
||||
RequestSide.Both,
|
||||
partyId,
|
||||
participantId,
|
||||
ParticipantPermission.Submission,
|
||||
replaceExisting = false,
|
||||
force = force,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Disable party on participant")
|
||||
def disable(name: Identifier, force: Boolean = false): Unit = {
|
||||
val partyId = PartyId(participantId.uid.copy(id = name))
|
||||
val _ = consoleEnvironment.run {
|
||||
runPartyCommand(partyId, TopologyChangeOp.Remove, force)
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Update participant-local party details")
|
||||
@Help.Description(
|
||||
"""Currently you can update only the annotations.
|
||||
|You cannot update other user attributes.
|
||||
party: party to be updated,
|
||||
modifier: a function to modify the party details, e.g.: `partyDetails => { partyDetails.copy(annotations = partyDetails.annotations.updated("a", "b").removed("c")) }`"""
|
||||
)
|
||||
def update(
|
||||
party: PartyId,
|
||||
modifier: PartyDetails => PartyDetails,
|
||||
): PartyDetails = {
|
||||
runner.ledger_api.parties.update(
|
||||
party = party,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Set party display name")
|
||||
@Help.Description(
|
||||
"Locally set the party display name (shown on the ledger-api) to the given value"
|
||||
)
|
||||
def set_display_name(party: PartyId, displayName: String): Unit = consoleEnvironment.run {
|
||||
// takes displayName as String argument which is validated at GrpcPartyNameManagementService
|
||||
runner.adminCommand(
|
||||
ParticipantAdminCommands.PartyNameManagement.SetPartyDisplayName(party, displayName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class LocalParticipantPartiesAdministrationGroup(
|
||||
reference: LocalParticipantReference,
|
||||
runner: AdminCommandRunner
|
||||
& BaseInspection[ParticipantNode]
|
||||
& ParticipantAdministration
|
||||
& BaseLedgerApiAdministration,
|
||||
val consoleEnvironment: ConsoleEnvironment,
|
||||
val loggerFactory: NamedLoggerFactory,
|
||||
) extends ParticipantPartiesAdministrationGroup(reference.id, runner, consoleEnvironment)
|
||||
with FeatureFlagFilter {
|
||||
|
||||
import runner.*
|
||||
|
||||
@Help.Summary("Waits for any topology changes to be observed", FeatureFlag.Preview)
|
||||
@Help.Description(
|
||||
"Will throw an exception if the given topology has not been observed within the given timeout."
|
||||
)
|
||||
def await_topology_observed[T <: ParticipantReference](
|
||||
partyAssignment: Set[(PartyId, T)],
|
||||
timeout: NonNegativeDuration = consoleEnvironment.commandTimeouts.bounded,
|
||||
)(implicit env: ConsoleEnvironment): Unit =
|
||||
check(FeatureFlag.Preview) {
|
||||
access(node =>
|
||||
TopologySynchronisation.awaitTopologyObserved(reference, partyAssignment, timeout)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object TopologySynchronisation {
|
||||
|
||||
def awaitTopologyObserved[T <: ParticipantReference](
|
||||
reference: ParticipantReference,
|
||||
partyAssignment: Set[(PartyId, T)],
|
||||
timeout: NonNegativeDuration,
|
||||
)(implicit env: ConsoleEnvironment): Unit =
|
||||
TraceContext.withNewTraceContext { _ =>
|
||||
ConsoleMacros.utils.retry_until_true(timeout) {
|
||||
val partiesWithId = partyAssignment.map { case (party, participantRef) =>
|
||||
(party, participantRef.id)
|
||||
}
|
||||
env.domains.all.forall { domain =>
|
||||
val domainId = domain.id
|
||||
!reference.domains.active(domain) || {
|
||||
val timestamp = reference.testing.fetch_domain_time(domainId)
|
||||
partiesWithId.subsetOf(
|
||||
reference.parties
|
||||
.list(asOf = Some(timestamp.toInstant))
|
||||
.flatMap(res => res.participants.map(par => (res.party, par.participant)))
|
||||
.toSet
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,430 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.foldable.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.digitalasset.canton.LedgerParticipantId
|
||||
import com.digitalasset.canton.admin.api.client.commands.{
|
||||
ParticipantAdminCommands,
|
||||
TopologyAdminCommands,
|
||||
TopologyAdminCommandsX,
|
||||
}
|
||||
import com.digitalasset.canton.admin.api.client.data.{
|
||||
ListConnectedDomainsResult,
|
||||
ListPartiesResult,
|
||||
PartyDetails,
|
||||
}
|
||||
import com.digitalasset.canton.config.CantonRequireTypes.String255
|
||||
import com.digitalasset.canton.config.NonNegativeDuration
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
BaseInspection,
|
||||
CantonInternalError,
|
||||
CommandFailure,
|
||||
ConsoleCommandResult,
|
||||
ConsoleEnvironment,
|
||||
ConsoleMacros,
|
||||
FeatureFlag,
|
||||
FeatureFlagFilter,
|
||||
Help,
|
||||
Helpful,
|
||||
InstanceReferenceX,
|
||||
LocalParticipantReferenceX,
|
||||
ParticipantReferenceX,
|
||||
}
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.participant.ParticipantNodeX
|
||||
import com.digitalasset.canton.topology.*
|
||||
import com.digitalasset.canton.topology.store.TopologyStoreId.AuthorizedStore
|
||||
import com.digitalasset.canton.topology.transaction.*
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.digitalasset.canton.util.ShowUtil.*
|
||||
|
||||
import java.time.Instant
|
||||
import scala.util.Try
|
||||
|
||||
class PartiesAdministrationGroupX(
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
) extends Helpful {
|
||||
|
||||
protected def defaultLimit: PositiveInt =
|
||||
consoleEnvironment.environment.config.parameters.console.defaultLimit
|
||||
|
||||
import runner.*
|
||||
|
||||
@Help.Summary(
|
||||
"List active parties, their active participants, and the participants' permissions on domains."
|
||||
)
|
||||
@Help.Description(
|
||||
"""Inspect the parties known by this participant as used for synchronisation.
|
||||
|The response is built from the timestamped topology transactions of each domain, excluding the
|
||||
|authorized store of the given node. For each known party, the list of active
|
||||
|participants and their permission on the domain for that party is given.
|
||||
|
|
||||
filterParty: Filter by parties starting with the given string.
|
||||
filterParticipant: Filter for parties that are hosted by a participant with an id starting with the given string
|
||||
filterDomain: Filter by domains whose id starts with the given string.
|
||||
asOf: Optional timestamp to inspect the topology state at a given point in time.
|
||||
limit: Limit on the number of parties fetched (defaults to canton.parameters.console.default-limit).
|
||||
|
||||
Example: participant1.parties.list(filterParty="alice")
|
||||
"""
|
||||
)
|
||||
def list(
|
||||
filterParty: String = "",
|
||||
filterParticipant: String = "",
|
||||
filterDomain: String = "",
|
||||
asOf: Option[Instant] = None,
|
||||
limit: PositiveInt = defaultLimit,
|
||||
): Seq[ListPartiesResult] =
|
||||
consoleEnvironment.run {
|
||||
adminCommand(
|
||||
TopologyAdminCommands.Aggregation.ListParties(
|
||||
filterDomain = filterDomain,
|
||||
filterParty = filterParty,
|
||||
filterParticipant = filterParticipant,
|
||||
asOf = asOf,
|
||||
limit = limit,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ParticipantPartiesAdministrationGroupX(
|
||||
participantId: => ParticipantId,
|
||||
runner: AdminCommandRunner
|
||||
& ParticipantAdministration
|
||||
& BaseLedgerApiAdministration
|
||||
& InstanceReferenceX,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
) extends PartiesAdministrationGroupX(runner, consoleEnvironment) {
|
||||
|
||||
@Help.Summary("List parties hosted by this participant")
|
||||
@Help.Description("""Inspect the parties hosted by this participant as used for synchronisation.
|
||||
|The response is built from the timestamped topology transactions of each domain, excluding the
|
||||
|authorized store of the given node. The search will include all hosted parties and is equivalent
|
||||
|to running the `list` method using the participant id of the invoking participant.
|
||||
|
|
||||
filterParty: Filter by parties starting with the given string.
|
||||
filterDomain: Filter by domains whose id starts with the given string.
|
||||
asOf: Optional timestamp to inspect the topology state at a given point in time.
|
||||
limit: How many items to return (defaults to canton.parameters.console.default-limit)
|
||||
|
||||
Example: participant1.parties.hosted(filterParty="alice")""")
|
||||
def hosted(
|
||||
filterParty: String = "",
|
||||
filterDomain: String = "",
|
||||
asOf: Option[Instant] = None,
|
||||
limit: PositiveInt = defaultLimit,
|
||||
): Seq[ListPartiesResult] = {
|
||||
list(
|
||||
filterParty,
|
||||
filterParticipant = participantId.filterString,
|
||||
filterDomain = filterDomain,
|
||||
asOf = asOf,
|
||||
limit = limit,
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Find a party from a filter string")
|
||||
@Help.Description(
|
||||
"""Will search for all parties that match this filter string. If it finds exactly one party, it
|
||||
|will return that one. Otherwise, the function will throw."""
|
||||
)
|
||||
def find(filterParty: String): PartyId = {
|
||||
list(filterParty).map(_.party).distinct.toList match {
|
||||
case one :: Nil => one
|
||||
case Nil => throw new IllegalArgumentException(s"No party matching $filterParty")
|
||||
case more =>
|
||||
throw new IllegalArgumentException(s"Multiple parties match $filterParty: $more")
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Enable/add party to participant")
|
||||
@Help.Description("""This function registers a new party with the current participant within the participants
|
||||
|namespace. The function fails if the participant does not have appropriate signing keys
|
||||
|to issue the corresponding PartyToParticipant topology transaction.
|
||||
|Optionally, a local display name can be added. This display name will be exposed on the
|
||||
|ledger API party management endpoint.
|
||||
|Specifying a set of domains via the `WaitForDomain` parameter ensures that the domains have
|
||||
|enabled/added a party by the time the call returns, but other participants connected to the same domains may not
|
||||
|yet be aware of the party.
|
||||
|Additionally, a sequence of additional participants can be added to be synchronized to
|
||||
|ensure that the party is known to these participants as well before the function terminates.
|
||||
|""")
|
||||
def enable(
|
||||
name: String,
|
||||
namespace: Namespace = participantId.uid.namespace,
|
||||
participants: Seq[ParticipantId] = Seq(participantId),
|
||||
threshold: PositiveInt = PositiveInt.one,
|
||||
displayName: Option[String] = None,
|
||||
// TODO(i10809) replace wait for domain for a clean topology synchronisation using the dispatcher info
|
||||
waitForDomain: DomainChoice = DomainChoice.Only(Seq()),
|
||||
synchronizeParticipants: Seq[ParticipantReferenceX] = Seq(),
|
||||
groupAddressing: Boolean = false,
|
||||
mustFullyAuthorize: Boolean = true,
|
||||
): PartyId = {
|
||||
|
||||
def registered(lst: => Seq[ListPartiesResult]): Set[DomainId] = {
|
||||
lst
|
||||
.flatMap(_.participants.flatMap(_.domains))
|
||||
.map(_.domain)
|
||||
.toSet
|
||||
}
|
||||
def primaryRegistered(partyId: PartyId) =
|
||||
registered(
|
||||
list(filterParty = partyId.filterString, filterParticipant = participantId.filterString)
|
||||
)
|
||||
|
||||
def primaryConnected: Either[String, Seq[ListConnectedDomainsResult]] =
|
||||
runner
|
||||
.adminCommand(ParticipantAdminCommands.DomainConnectivity.ListConnectedDomains())
|
||||
.toEither
|
||||
|
||||
def findDomainIds(
|
||||
name: String,
|
||||
connected: Either[String, Seq[ListConnectedDomainsResult]],
|
||||
): Either[String, Set[DomainId]] = {
|
||||
for {
|
||||
domainIds <- waitForDomain match {
|
||||
case DomainChoice.All =>
|
||||
connected.map(_.map(_.domainId))
|
||||
case DomainChoice.Only(Seq()) =>
|
||||
Right(Seq())
|
||||
case DomainChoice.Only(aliases) =>
|
||||
connected.flatMap { res =>
|
||||
val connectedM = res.map(x => (x.domainAlias, x.domainId)).toMap
|
||||
aliases.traverse(alias => connectedM.get(alias).toRight(s"Unknown: $alias for $name"))
|
||||
}
|
||||
}
|
||||
} yield domainIds.toSet
|
||||
}
|
||||
def retryE(condition: => Boolean, message: => String): Either[String, Unit] = {
|
||||
AdminCommandRunner
|
||||
.retryUntilTrue(consoleEnvironment.commandTimeouts.ledgerCommand)(condition)
|
||||
.toEither
|
||||
.leftMap(_ => message)
|
||||
}
|
||||
def waitForParty(
|
||||
partyId: PartyId,
|
||||
domainIds: Set[DomainId],
|
||||
registered: => Set[DomainId],
|
||||
queriedParticipant: ParticipantId = participantId,
|
||||
): Either[String, Unit] = {
|
||||
if (domainIds.nonEmpty) {
|
||||
retryE(
|
||||
domainIds subsetOf registered,
|
||||
show"Party $partyId did not appear for $queriedParticipant on domain ${domainIds.diff(registered)}",
|
||||
)
|
||||
} else Right(())
|
||||
}
|
||||
val syncLedgerApi = waitForDomain match {
|
||||
case DomainChoice.All => true
|
||||
case DomainChoice.Only(aliases) => aliases.nonEmpty
|
||||
}
|
||||
consoleEnvironment.run {
|
||||
ConsoleCommandResult.fromEither {
|
||||
for {
|
||||
// validating party and display name here to prevent, e.g., a party being registered despite it having an invalid display name
|
||||
// assert that name is valid ParticipantId
|
||||
id <- Identifier.create(name)
|
||||
partyId = PartyId(id, namespace)
|
||||
_ <- Either
|
||||
.catchOnly[IllegalArgumentException](LedgerParticipantId.assertFromString(name))
|
||||
.leftMap(_.getMessage)
|
||||
validDisplayName <- displayName.map(String255.create(_, Some("display name"))).sequence
|
||||
// find the domain ids
|
||||
domainIds <- findDomainIds(this.participantId.uid.id.unwrap, primaryConnected)
|
||||
// find the domain ids the additional participants are connected to
|
||||
additionalSync <- synchronizeParticipants.traverse { p =>
|
||||
findDomainIds(
|
||||
p.name,
|
||||
Try(p.domains.list_connected()).toEither.leftMap {
|
||||
case exception @ (_: CommandFailure | _: CantonInternalError) =>
|
||||
exception.getMessage
|
||||
case exception => throw exception
|
||||
},
|
||||
)
|
||||
.map(domains => (p, domains intersect domainIds))
|
||||
}
|
||||
_ <- runPartyCommand(
|
||||
partyId,
|
||||
participants,
|
||||
threshold,
|
||||
groupAddressing,
|
||||
mustFullyAuthorize,
|
||||
).toEither
|
||||
_ <- validDisplayName match {
|
||||
case None => Right(())
|
||||
case Some(name) =>
|
||||
runner
|
||||
.adminCommand(
|
||||
ParticipantAdminCommands.PartyNameManagement
|
||||
.SetPartyDisplayName(partyId, name.unwrap)
|
||||
)
|
||||
.toEither
|
||||
}
|
||||
_ <- waitForParty(partyId, domainIds, primaryRegistered(partyId))
|
||||
_ <-
|
||||
// sync with ledger-api server if this node is connected to at least one domain
|
||||
if (syncLedgerApi && primaryConnected.exists(_.nonEmpty))
|
||||
retryE(
|
||||
runner.ledger_api.parties.list().map(_.party).contains(partyId),
|
||||
show"The party $partyId never appeared on the ledger API server",
|
||||
)
|
||||
else Right(())
|
||||
_ <- additionalSync.traverse_ { case (p, domains) =>
|
||||
waitForParty(
|
||||
partyId,
|
||||
domains,
|
||||
registered(
|
||||
p.parties.list(
|
||||
filterParty = partyId.filterString,
|
||||
filterParticipant = participantId.filterString,
|
||||
)
|
||||
),
|
||||
p.id,
|
||||
)
|
||||
}
|
||||
} yield partyId
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def runPartyCommand(
|
||||
partyId: PartyId,
|
||||
participants: Seq[ParticipantId],
|
||||
threshold: PositiveInt,
|
||||
groupAddressing: Boolean,
|
||||
mustFullyAuthorize: Boolean,
|
||||
): ConsoleCommandResult[SignedTopologyTransactionX[TopologyChangeOpX, PartyToParticipantX]] = {
|
||||
|
||||
runner
|
||||
.adminCommand(
|
||||
TopologyAdminCommandsX.Write.Propose(
|
||||
// TODO(#14048) properly set the serial or introduce auto-detection so we don't
|
||||
// have to set it on the client side
|
||||
mapping = PartyToParticipantX(
|
||||
partyId,
|
||||
None,
|
||||
threshold,
|
||||
participants.map(pid =>
|
||||
HostingParticipant(
|
||||
pid,
|
||||
if (threshold.value > 1) ParticipantPermissionX.Confirmation
|
||||
else ParticipantPermissionX.Submission,
|
||||
)
|
||||
),
|
||||
groupAddressing,
|
||||
),
|
||||
signedBy = Seq(this.participantId.uid.namespace.fingerprint),
|
||||
serial = None,
|
||||
store = AuthorizedStore.filterName,
|
||||
mustFullyAuthorize = mustFullyAuthorize,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Disable party on participant")
|
||||
// TODO(#14067): reintroduce `force` once it is implemented on the server side and threaded through properly.
|
||||
def disable(name: Identifier /*, force: Boolean = false*/ ): Unit = {
|
||||
runner.topology.party_to_participant_mappings
|
||||
.propose_delta(
|
||||
PartyId(name, runner.id.member.uid.namespace),
|
||||
removes = List(this.participantId),
|
||||
)
|
||||
.discard
|
||||
}
|
||||
|
||||
@Help.Summary("Update participant-local party details")
|
||||
@Help.Description(
|
||||
"""Currently you can update only the annotations.
|
||||
|You cannot update other user attributes.
|
||||
party: party to be updated,
|
||||
modifier: a function to modify the party details, e.g.: `partyDetails => { partyDetails.copy(annotations = partyDetails.annotations.updated("a", "b").removed("c")) }`"""
|
||||
)
|
||||
def update(
|
||||
party: PartyId,
|
||||
modifier: PartyDetails => PartyDetails,
|
||||
): PartyDetails = {
|
||||
runner.ledger_api.parties.update(
|
||||
party = party,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Set party display name")
|
||||
@Help.Description(
|
||||
"Locally set the party display name (shown on the ledger-api) to the given value"
|
||||
)
|
||||
def set_display_name(party: PartyId, displayName: String): Unit = consoleEnvironment.run {
|
||||
// takes displayName as String argument which is validated at GrpcPartyNameManagementService
|
||||
runner.adminCommand(
|
||||
ParticipantAdminCommands.PartyNameManagement.SetPartyDisplayName(party, displayName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class LocalParticipantPartiesAdministrationGroupX(
|
||||
reference: LocalParticipantReferenceX,
|
||||
runner: AdminCommandRunner
|
||||
& BaseInspection[ParticipantNodeX]
|
||||
& ParticipantAdministration
|
||||
& BaseLedgerApiAdministration
|
||||
& InstanceReferenceX,
|
||||
val consoleEnvironment: ConsoleEnvironment,
|
||||
val loggerFactory: NamedLoggerFactory,
|
||||
) extends ParticipantPartiesAdministrationGroupX(reference.id, runner, consoleEnvironment)
|
||||
with FeatureFlagFilter {
|
||||
|
||||
import runner.*
|
||||
|
||||
@Help.Summary("Waits for any topology changes to be observed", FeatureFlag.Preview)
|
||||
@Help.Description(
|
||||
"Will throw an exception if the given topology has not been observed within the given timeout."
|
||||
)
|
||||
def await_topology_observed[T <: ParticipantReferenceX](
|
||||
partyAssignment: Set[(PartyId, T)],
|
||||
timeout: NonNegativeDuration = consoleEnvironment.commandTimeouts.bounded,
|
||||
)(implicit env: ConsoleEnvironment): Unit =
|
||||
check(FeatureFlag.Preview) {
|
||||
access(node =>
|
||||
TopologySynchronisationX.awaitTopologyObserved(reference, partyAssignment, timeout)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object TopologySynchronisationX {
|
||||
|
||||
def awaitTopologyObserved[T <: ParticipantReferenceX](
|
||||
reference: ParticipantReferenceX,
|
||||
partyAssignment: Set[(PartyId, T)],
|
||||
timeout: NonNegativeDuration,
|
||||
)(implicit env: ConsoleEnvironment): Unit =
|
||||
TraceContext.withNewTraceContext { _ =>
|
||||
ConsoleMacros.utils.retry_until_true(timeout) {
|
||||
val partiesWithId = partyAssignment.map { case (party, participantRef) =>
|
||||
(party, participantRef.id)
|
||||
}
|
||||
env.domains.all.forall { domain =>
|
||||
val domainId = domain.id
|
||||
!reference.domains.active(domain) || {
|
||||
val timestamp = reference.testing.fetch_domain_time(domainId)
|
||||
partiesWithId.subsetOf(
|
||||
reference.parties
|
||||
.list(asOf = Some(timestamp.toInstant))
|
||||
.flatMap(res => res.participants.map(par => (res.party, par.participant)))
|
||||
.toSet
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.commands.PruningSchedulerCommands
|
||||
import com.digitalasset.canton.admin.api.client.data.PruningSchedule
|
||||
import com.digitalasset.canton.config.PositiveDurationSeconds
|
||||
import com.digitalasset.canton.console.{AdminCommandRunner, ConsoleEnvironment, Help, Helpful}
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import io.grpc.stub.AbstractStub
|
||||
|
||||
/** Pruning scheduler administration api shared by participant/mediator/sequencer.
|
||||
*/
|
||||
class PruningSchedulerAdministration[T <: AbstractStub[T]](
|
||||
runner: AdminCommandRunner,
|
||||
protected val consoleEnvironment: ConsoleEnvironment,
|
||||
commands: PruningSchedulerCommands[T],
|
||||
protected val loggerFactory: NamedLoggerFactory,
|
||||
) extends Helpful {
|
||||
|
||||
@Help.Summary(
|
||||
"Activate automatic pruning according to the specified schedule."
|
||||
)
|
||||
@Help.Description(
|
||||
"""The schedule is specified in cron format and "max_duration" and "retention" durations. The cron string indicates
|
||||
|the points in time at which pruning should begin in the GMT time zone, and the maximum duration indicates how
|
||||
|long from the start time pruning is allowed to run as long as pruning has not finished pruning up to the
|
||||
|specified retention period.
|
||||
"""
|
||||
)
|
||||
def set_schedule(
|
||||
cron: String,
|
||||
maxDuration: PositiveDurationSeconds,
|
||||
retention: PositiveDurationSeconds,
|
||||
): Unit =
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(
|
||||
commands.SetScheduleCommand(cron = cron, maxDuration = maxDuration, retention = retention)
|
||||
)
|
||||
)
|
||||
|
||||
@Help.Summary("Deactivate automatic pruning.")
|
||||
def clear_schedule(): Unit =
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(commands.ClearScheduleCommand())
|
||||
)
|
||||
|
||||
@Help.Summary("Modify the cron used by automatic pruning.")
|
||||
@Help.Description(
|
||||
"""The schedule is specified in cron format and refers to pruning start times in the GMT time zone.
|
||||
|This call returns an error if no schedule has been configured via `set_schedule` or if automatic
|
||||
|pruning has been disabled via `clear_schedule`. Additionally if at the time of this modification, pruning is
|
||||
|actively running, a best effort is made to pause pruning and restart according to the new schedule. This
|
||||
|allows for the case that the new schedule no longer allows pruning at the current time.
|
||||
"""
|
||||
)
|
||||
def set_cron(cron: String): Unit =
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(commands.SetCronCommand(cron))
|
||||
)
|
||||
|
||||
@Help.Summary("Modify the maximum duration used by automatic pruning.")
|
||||
@Help.Description(
|
||||
"""The `maxDuration` is specified as a positive duration and has at most per-second granularity.
|
||||
|This call returns an error if no schedule has been configured via `set_schedule` or if automatic
|
||||
|pruning has been disabled via `clear_schedule`. Additionally if at the time of this modification, pruning is
|
||||
|actively running, a best effort is made to pause pruning and restart according to the new schedule. This
|
||||
|allows for the case that the new schedule no longer allows pruning at the current time.
|
||||
"""
|
||||
)
|
||||
def set_max_duration(maxDuration: PositiveDurationSeconds): Unit =
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(
|
||||
commands.SetMaxDurationCommand(maxDuration)
|
||||
)
|
||||
)
|
||||
|
||||
@Help.Summary("Update the pruning retention used by automatic pruning.")
|
||||
@Help.Description(
|
||||
"""The `retention` is specified as a positive duration and has at most per-second granularity.
|
||||
|This call returns an error if no schedule has been configured via `set_schedule` or if automatic
|
||||
|pruning has been disabled via `clear_schedule`. Additionally if at the time of this update, pruning is
|
||||
|actively running, a best effort is made to pause pruning and restart with the newly specified retention.
|
||||
|This allows for the case that the new retention mandates retaining more data than previously.
|
||||
"""
|
||||
)
|
||||
def set_retention(retention: PositiveDurationSeconds): Unit =
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(
|
||||
commands.SetRetentionCommand(retention)
|
||||
)
|
||||
)
|
||||
|
||||
@Help.Summary("Inspect the automatic pruning schedule.")
|
||||
@Help.Description(
|
||||
"""The schedule consists of a "cron" expression and "max_duration" and "retention" durations. The cron string
|
||||
|indicates the points in time at which pruning should begin in the GMT time zone, and the maximum duration
|
||||
|indicates how long from the start time pruning is allowed to run as long as pruning has not finished pruning
|
||||
|up to the specified retention period.
|
||||
|Returns `None` if no schedule has been configured via `set_schedule` or if `clear_schedule` has been invoked.
|
||||
"""
|
||||
)
|
||||
def get_schedule(): Option[PruningSchedule] =
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(commands.GetScheduleCommand())
|
||||
)
|
||||
|
||||
}
|
@ -0,0 +1,274 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.commands.EnterpriseSequencerAdminCommands.LocatePruningTimestampCommand
|
||||
import com.digitalasset.canton.admin.api.client.commands.{
|
||||
EnterpriseSequencerAdminCommands,
|
||||
PruningSchedulerCommands,
|
||||
SequencerAdminCommands,
|
||||
}
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
ConsoleEnvironment,
|
||||
FeatureFlag,
|
||||
Help,
|
||||
Helpful,
|
||||
}
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.domain.admin.v0.EnterpriseSequencerAdministrationServiceGrpc
|
||||
import com.digitalasset.canton.domain.admin.v0.EnterpriseSequencerAdministrationServiceGrpc.EnterpriseSequencerAdministrationServiceStub
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.{
|
||||
SequencerClients,
|
||||
SequencerPruningStatus,
|
||||
SequencerSnapshot,
|
||||
}
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.time.EnrichedDurations.*
|
||||
import com.digitalasset.canton.topology.Member
|
||||
import com.digitalasset.canton.util.ShowUtil.*
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.jdk.DurationConverters.*
|
||||
|
||||
trait SequencerAdministrationGroupCommon extends ConsoleCommandGroup {
|
||||
|
||||
@Help.Summary("Pruning of the sequencer")
|
||||
object pruning
|
||||
extends PruningSchedulerAdministration(
|
||||
runner,
|
||||
consoleEnvironment,
|
||||
new PruningSchedulerCommands[EnterpriseSequencerAdministrationServiceStub](
|
||||
EnterpriseSequencerAdministrationServiceGrpc.stub,
|
||||
_.setSchedule(_),
|
||||
_.clearSchedule(_),
|
||||
_.setCron(_),
|
||||
_.setMaxDuration(_),
|
||||
_.setRetention(_),
|
||||
_.getSchedule(_),
|
||||
),
|
||||
loggerFactory,
|
||||
)
|
||||
with Helpful {
|
||||
@Help.Summary("Status of the sequencer and its connected clients")
|
||||
@Help.Description(
|
||||
"""Provides a detailed breakdown of information required for pruning:
|
||||
| - the current time according to this sequencer instance
|
||||
| - domain members that the sequencer supports
|
||||
| - for each member when they were registered and whether they are enabled
|
||||
| - a list of clients for each member, their last acknowledgement, and whether they are enabled
|
||||
|"""
|
||||
)
|
||||
def status(): SequencerPruningStatus =
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(SequencerAdminCommands.GetPruningStatus)
|
||||
}
|
||||
|
||||
@Help.Summary("Remove unnecessary data from the Sequencer up until the default retention point")
|
||||
@Help.Description(
|
||||
"""Removes unnecessary data from the Sequencer that is earlier than the default retention period.
|
||||
|The default retention period is set in the configuration of the canton processing running this
|
||||
|command under `parameters.retention-period-defaults.sequencer`.
|
||||
|This pruning command requires that data is read and acknowledged by clients before
|
||||
|considering it safe to remove.
|
||||
|
|
||||
|If no data is being removed it could indicate that clients are not reading or acknowledging data
|
||||
|in a timely fashion (typically due to nodes going offline for long periods).
|
||||
|You have the option of disabling the members running on these nodes to allow removal of this data,
|
||||
|however this will mean that they will be unable to reconnect to the domain in the future.
|
||||
|To do this run `force_prune(dryRun = true)` to return a description of which members would be
|
||||
|disabled in order to prune the Sequencer.
|
||||
|If you are happy to disable the described clients then run `force_prune(dryRun = false)` to
|
||||
|permanently remove their unread data.
|
||||
|
|
||||
|Once offline clients have been disabled you can continue to run `prune` normally.
|
||||
|"""
|
||||
)
|
||||
def prune(): String = {
|
||||
val defaultRetention =
|
||||
consoleEnvironment.environment.config.parameters.retentionPeriodDefaults.sequencer
|
||||
prune_with_retention_period(defaultRetention.underlying)
|
||||
}
|
||||
|
||||
@Help.Summary(
|
||||
"Force remove data from the Sequencer including data that may have not been read by offline clients"
|
||||
)
|
||||
@Help.Description(
|
||||
"""Will force pruning up until the default retention period by potentially disabling clients
|
||||
|that have not yet read data we would like to remove.
|
||||
|Disabling these clients will prevent them from ever reconnecting to the Domain so should only be
|
||||
|used if the Domain operator is confident they can be permanently ignored.
|
||||
|Run with `dryRun = true` to review a description of which clients will be disabled first.
|
||||
|Run with `dryRun = false` to disable these clients and perform a forced pruning.
|
||||
|"""
|
||||
)
|
||||
def force_prune(dryRun: Boolean): String = {
|
||||
val defaultRetention =
|
||||
consoleEnvironment.environment.config.parameters.retentionPeriodDefaults.sequencer
|
||||
force_prune_with_retention_period(defaultRetention.underlying, dryRun)
|
||||
}
|
||||
|
||||
@Help.Summary("Remove data that has been read up until a custom retention period")
|
||||
@Help.Description(
|
||||
"Similar to the above `prune` command but allows specifying a custom retention period"
|
||||
)
|
||||
def prune_with_retention_period(retentionPeriod: FiniteDuration): String = {
|
||||
val status = this.status()
|
||||
val pruningTimestamp = status.now.minus(retentionPeriod.toJava)
|
||||
|
||||
prune_at(pruningTimestamp)
|
||||
}
|
||||
|
||||
@Help.Summary(
|
||||
"Force removing data from the Sequencer including data that may have not been read by offline clients up until a custom retention period"
|
||||
)
|
||||
@Help.Description(
|
||||
"Similar to the above `force_prune` command but allows specifying a custom retention period"
|
||||
)
|
||||
def force_prune_with_retention_period(
|
||||
retentionPeriod: FiniteDuration,
|
||||
dryRun: Boolean,
|
||||
): String = {
|
||||
val status = this.status()
|
||||
val pruningTimestamp = status.now.minus(retentionPeriod.toJava)
|
||||
|
||||
force_prune_at(pruningTimestamp, dryRun)
|
||||
}
|
||||
|
||||
@Help.Summary("Remove data that has been read up until the specified time")
|
||||
@Help.Description(
|
||||
"""Similar to the above `prune` command but allows specifying the exact time at which to prune.
|
||||
|The command will fail if a client has not yet read and acknowledged some data up to the specified time."""
|
||||
)
|
||||
def prune_at(timestamp: CantonTimestamp): String = {
|
||||
val status = this.status()
|
||||
val unauthenticatedMembers =
|
||||
status.unauthenticatedMembersToDisable(
|
||||
consoleEnvironment.environment.config.parameters.retentionPeriodDefaults.unauthenticatedMembers.toInternal
|
||||
)
|
||||
unauthenticatedMembers.foreach(disable_member)
|
||||
val msg = consoleEnvironment.run {
|
||||
runner.adminCommand(EnterpriseSequencerAdminCommands.Prune(timestamp))
|
||||
}
|
||||
s"$msg. Automatically disabled ${unauthenticatedMembers.size} unauthenticated member clients."
|
||||
}
|
||||
|
||||
@Help.Summary(
|
||||
"Force removing data from the Sequencer including data that may have not been read by offline clients up until the specified time"
|
||||
)
|
||||
@Help.Description(
|
||||
"Similar to the above `force_prune` command but allows specifying the exact time at which to prune"
|
||||
)
|
||||
def force_prune_at(timestamp: CantonTimestamp, dryRun: Boolean): String = {
|
||||
val initialStatus = status()
|
||||
val clientsToDisable = initialStatus.clientsPreventingPruning(timestamp)
|
||||
|
||||
if (dryRun) {
|
||||
formatDisableDryRun(timestamp, clientsToDisable)
|
||||
} else {
|
||||
disableClients(clientsToDisable)
|
||||
|
||||
// check we can now prune for the provided timestamp
|
||||
val statusAfterDisabling = status()
|
||||
val safeTimestamp = statusAfterDisabling.safePruningTimestamp
|
||||
|
||||
if (safeTimestamp < timestamp)
|
||||
sys.error(
|
||||
s"We disabled all clients preventing pruning at $timestamp however the safe timestamp is set to $safeTimestamp"
|
||||
)
|
||||
|
||||
prune_at(timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
private def disableClients(toDisable: SequencerClients): Unit =
|
||||
toDisable.members.foreach(disable_member)
|
||||
|
||||
private def formatDisableDryRun(
|
||||
timestamp: CantonTimestamp,
|
||||
toDisable: SequencerClients,
|
||||
): String = {
|
||||
val toDisableText =
|
||||
toDisable.members.toSeq.map(member => show"- $member").map(m => s" $m (member)").sorted
|
||||
|
||||
if (toDisableText.isEmpty) {
|
||||
show"The Sequencer can be safely pruned for $timestamp without disabling clients"
|
||||
} else {
|
||||
val sb = new StringBuilder()
|
||||
sb.append(s"To prune the Sequencer at $timestamp we will disable:")
|
||||
toDisableText foreach { item =>
|
||||
sb.append(System.lineSeparator())
|
||||
sb.append(item)
|
||||
}
|
||||
sb.append(System.lineSeparator())
|
||||
sb.append(
|
||||
"To disable these clients to allow for pruning at this point run force_prune with dryRun set to false"
|
||||
)
|
||||
sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Obtain a timestamp at or near the beginning of sequencer state")
|
||||
@Help.Description(
|
||||
"""This command provides insight into the current state of sequencer pruning when called with
|
||||
|the default value of `index` 1.
|
||||
|When pruning the sequencer manually via `prune_at` and with the intent to prune in batches, specify
|
||||
|a value such as 1000 to obtain a pruning timestamp that corresponds to the "end" of the batch."""
|
||||
)
|
||||
def locate_pruning_timestamp(
|
||||
index: PositiveInt = PositiveInt.tryCreate(1)
|
||||
): Option[CantonTimestamp] =
|
||||
check(FeatureFlag.Preview) {
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(LocatePruningTimestampCommand(index))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected def disable_member(member: Member): Unit
|
||||
|
||||
}
|
||||
|
||||
trait SequencerAdministrationDisableMember extends ConsoleCommandGroup {
|
||||
|
||||
/** Disable the provided member at the sequencer preventing them from reading and writing, and allowing their
|
||||
* data to be pruned.
|
||||
*/
|
||||
@Help.Summary(
|
||||
"Disable the provided member at the Sequencer that will allow any unread data for them to be removed"
|
||||
)
|
||||
@Help.Description("""This will prevent any client for the given member to reconnect the Sequencer
|
||||
|and allow any unread/unacknowledged data they have to be removed.
|
||||
|This should only be used if the domain operation is confident the member will never need
|
||||
|to reconnect as there is no way to re-enable the member.
|
||||
|To view members using the sequencer run `sequencer.status()`."""")
|
||||
def disable_member(member: Member): Unit = consoleEnvironment.run {
|
||||
runner.adminCommand(EnterpriseSequencerAdminCommands.DisableMember(member))
|
||||
}
|
||||
}
|
||||
|
||||
class SequencerAdministrationGroup(
|
||||
val runner: AdminCommandRunner,
|
||||
val consoleEnvironment: ConsoleEnvironment,
|
||||
val loggerFactory: NamedLoggerFactory,
|
||||
) extends SequencerAdministrationGroupCommon
|
||||
with SequencerAdministrationDisableMember {
|
||||
|
||||
/** Snapshot based on given snapshot to used as initial state by other sequencer nodes in the process of onboarding.
|
||||
*/
|
||||
def snapshot(timestamp: CantonTimestamp): SequencerSnapshot =
|
||||
consoleEnvironment.run {
|
||||
runner.adminCommand(EnterpriseSequencerAdminCommands.Snapshot(timestamp))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait SequencerAdministrationGroupX extends SequencerAdministrationGroupCommon {
|
||||
|
||||
@Help.Summary("Methods used for repairing the node")
|
||||
object repair extends ConsoleCommandGroup.Impl(this) with SequencerAdministrationDisableMember {}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.commands.ParticipantAdminCommands
|
||||
import com.digitalasset.canton.config.RequireTypes.{PositiveInt, PositiveLong}
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
ConsoleEnvironment,
|
||||
FeatureFlagFilter,
|
||||
Help,
|
||||
Helpful,
|
||||
InstanceReferenceX,
|
||||
}
|
||||
import com.digitalasset.canton.crypto.Fingerprint
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.topology.*
|
||||
import com.digitalasset.canton.topology.transaction.{
|
||||
SignedTopologyTransactionX,
|
||||
TopologyChangeOpX,
|
||||
TrafficControlStateX,
|
||||
}
|
||||
import com.digitalasset.canton.traffic.MemberTrafficStatus
|
||||
|
||||
class TrafficControlAdministrationGroup(
|
||||
instance: InstanceReferenceX,
|
||||
topology: TopologyAdministrationGroupX,
|
||||
runner: AdminCommandRunner,
|
||||
override val consoleEnvironment: ConsoleEnvironment,
|
||||
override val loggerFactory: NamedLoggerFactory,
|
||||
) extends Helpful
|
||||
with FeatureFlagFilter {
|
||||
|
||||
@Help.Summary("Return the traffic state of the node")
|
||||
@Help.Description(
|
||||
"""Use this command to get the traffic state of the node at a given time for a specific domain ID."""
|
||||
)
|
||||
def traffic_state(
|
||||
domainId: DomainId
|
||||
): MemberTrafficStatus = {
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(
|
||||
ParticipantAdminCommands.TrafficControl
|
||||
.GetTrafficControlState(domainId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Top up traffic for this node")
|
||||
@Help.Description(
|
||||
"""Use this command to update the new total traffic limit for the node."""
|
||||
)
|
||||
def top_up(
|
||||
domainId: DomainId,
|
||||
newTotalTrafficAmount: PositiveLong,
|
||||
member: Member = instance.id.member,
|
||||
serial: Option[PositiveInt] = None,
|
||||
signedBy: Option[Fingerprint] = Some(instance.id.uid.namespace.fingerprint),
|
||||
): SignedTopologyTransactionX[TopologyChangeOpX, TrafficControlStateX] = {
|
||||
topology.traffic_control.top_up(
|
||||
domainId,
|
||||
newTotalTrafficAmount,
|
||||
member,
|
||||
serial,
|
||||
signedBy,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.commands.SequencerAdminCommands
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
ConsoleEnvironment,
|
||||
FeatureFlag,
|
||||
FeatureFlagFilter,
|
||||
Help,
|
||||
Helpful,
|
||||
InstanceReferenceX,
|
||||
}
|
||||
import com.digitalasset.canton.domain.sequencing.sequencer.traffic.SequencerTrafficStatus
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.topology.*
|
||||
|
||||
class TrafficControlSequencerAdministrationGroup(
|
||||
instance: InstanceReferenceX,
|
||||
topology: TopologyAdministrationGroupX,
|
||||
runner: AdminCommandRunner,
|
||||
override val consoleEnvironment: ConsoleEnvironment,
|
||||
override val loggerFactory: NamedLoggerFactory,
|
||||
) extends TrafficControlAdministrationGroup(
|
||||
instance,
|
||||
topology,
|
||||
runner,
|
||||
consoleEnvironment,
|
||||
loggerFactory,
|
||||
)
|
||||
with Helpful
|
||||
with FeatureFlagFilter {
|
||||
|
||||
@Help.Summary("Return the traffic state of the given members")
|
||||
@Help.Description(
|
||||
"""Use this command to get the traffic state of a list of members."""
|
||||
)
|
||||
def traffic_state_of_members(
|
||||
members: Seq[Member]
|
||||
): SequencerTrafficStatus = {
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(
|
||||
SequencerAdminCommands.GetTrafficControlState(members)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Return the traffic state of the all members")
|
||||
@Help.Description(
|
||||
"""Use this command to get the traffic state of all members."""
|
||||
)
|
||||
def traffic_state_of_all_members: SequencerTrafficStatus = {
|
||||
check(FeatureFlag.Preview)(
|
||||
consoleEnvironment.run(
|
||||
runner.adminCommand(
|
||||
SequencerAdminCommands.GetTrafficControlState(Seq.empty)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,613 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console.commands
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.either.*
|
||||
import com.digitalasset.canton.admin.api.client.commands.{TopologyAdminCommands, VaultAdminCommands}
|
||||
import com.digitalasset.canton.admin.api.client.data.ListKeyOwnersResult
|
||||
import com.digitalasset.canton.config.RequireTypes.PositiveInt
|
||||
import com.digitalasset.canton.console.{
|
||||
AdminCommandRunner,
|
||||
ConsoleEnvironment,
|
||||
FeatureFlag,
|
||||
FeatureFlagFilter,
|
||||
Help,
|
||||
Helpful,
|
||||
InstanceReferenceCommon,
|
||||
}
|
||||
import com.digitalasset.canton.crypto.*
|
||||
import com.digitalasset.canton.crypto.admin.grpc.PrivateKeyMetadata
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.time.Clock
|
||||
import com.digitalasset.canton.topology.store.TopologyStoreId.AuthorizedStore
|
||||
import com.digitalasset.canton.topology.{Member, MemberCode}
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.digitalasset.canton.util.{BinaryFileUtil, OptionUtil}
|
||||
import com.digitalasset.canton.version.ProtocolVersion
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.attribute.PosixFilePermission.{OWNER_READ, OWNER_WRITE}
|
||||
import java.time.Instant
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
class SecretKeyAdministration(
|
||||
instance: InstanceReferenceCommon,
|
||||
runner: AdminCommandRunner,
|
||||
override protected val consoleEnvironment: ConsoleEnvironment,
|
||||
override protected val loggerFactory: NamedLoggerFactory,
|
||||
) extends Helpful
|
||||
with FeatureFlagFilter {
|
||||
|
||||
import runner.*
|
||||
|
||||
protected def regenerateKey(currentKey: PublicKey, name: Option[String]): PublicKey = {
|
||||
currentKey match {
|
||||
case encKey: EncryptionPublicKey =>
|
||||
instance.keys.secret.generate_encryption_key(
|
||||
scheme = Some(encKey.scheme),
|
||||
name = OptionUtil.noneAsEmptyString(name),
|
||||
)
|
||||
case signKey: SigningPublicKey =>
|
||||
instance.keys.secret.generate_signing_key(
|
||||
scheme = Some(signKey.scheme),
|
||||
name = OptionUtil.noneAsEmptyString(name),
|
||||
)
|
||||
case unknown => throw new IllegalArgumentException(s"Invalid public key type: $unknown")
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("List keys in private vault")
|
||||
@Help.Description("""Returns all public keys to the corresponding private keys in the key vault.
|
||||
|Optional arguments can be used for filtering.""")
|
||||
def list(
|
||||
filterFingerprint: String = "",
|
||||
filterName: String = "",
|
||||
purpose: Set[KeyPurpose] = Set.empty,
|
||||
): Seq[PrivateKeyMetadata] =
|
||||
consoleEnvironment.run {
|
||||
adminCommand(VaultAdminCommands.ListMyKeys(filterFingerprint, filterName, purpose))
|
||||
}
|
||||
|
||||
@Help.Summary("Generate new public/private key pair for signing and store it in the vault")
|
||||
@Help.Description(
|
||||
"""
|
||||
|The optional name argument allows you to store an associated string for your convenience.
|
||||
|The scheme can be used to select a key scheme and the default scheme is used if left unspecified."""
|
||||
)
|
||||
def generate_signing_key(
|
||||
name: String = "",
|
||||
scheme: Option[SigningKeyScheme] = None,
|
||||
): SigningPublicKey = {
|
||||
consoleEnvironment.run {
|
||||
adminCommand(VaultAdminCommands.GenerateSigningKey(name, scheme))
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Generate new public/private key pair for encryption and store it in the vault")
|
||||
@Help.Description(
|
||||
"""
|
||||
|The optional name argument allows you to store an associated string for your convenience.
|
||||
|The scheme can be used to select a key scheme and the default scheme is used if left unspecified."""
|
||||
)
|
||||
def generate_encryption_key(
|
||||
name: String = "",
|
||||
scheme: Option[EncryptionKeyScheme] = None,
|
||||
): EncryptionPublicKey = {
|
||||
consoleEnvironment.run {
|
||||
adminCommand(VaultAdminCommands.GenerateEncryptionKey(name, scheme))
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary(
|
||||
"Register the specified KMS signing key in canton storing its public information in the vault"
|
||||
)
|
||||
@Help.Description(
|
||||
"""
|
||||
|The id for the KMS signing key.
|
||||
|The optional name argument allows you to store an associated string for your convenience."""
|
||||
)
|
||||
def register_kms_signing_key(
|
||||
kmsKeyId: String,
|
||||
name: String = "",
|
||||
): SigningPublicKey = {
|
||||
consoleEnvironment.run {
|
||||
adminCommand(VaultAdminCommands.RegisterKmsSigningKey(kmsKeyId, name))
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary(
|
||||
"Register the specified KMS encryption key in canton storing its public information in the vault"
|
||||
)
|
||||
@Help.Description(
|
||||
"""
|
||||
|The id for the KMS encryption key.
|
||||
|The optional name argument allows you to store an associated string for your convenience."""
|
||||
)
|
||||
def register_kms_encryption_key(
|
||||
kmsKeyId: String,
|
||||
name: String = "",
|
||||
): EncryptionPublicKey = {
|
||||
consoleEnvironment.run {
|
||||
adminCommand(VaultAdminCommands.RegisterKmsEncryptionKey(kmsKeyId, name))
|
||||
}
|
||||
}
|
||||
|
||||
private def findPublicKey(
|
||||
fingerprint: String,
|
||||
topologyAdmin: TopologyAdministrationGroupCommon,
|
||||
owner: Member,
|
||||
): PublicKey =
|
||||
findPublicKeys(topologyAdmin, owner).find(_.fingerprint.unwrap == fingerprint) match {
|
||||
case Some(key) => key
|
||||
case None =>
|
||||
throw new IllegalStateException(
|
||||
s"The key $fingerprint does not exist"
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Rotate a given node's keypair with a new pre-generated KMS keypair")
|
||||
@Help.Description(
|
||||
"""Rotates an existing encryption or signing key stored externally in a KMS with a pre-generated
|
||||
key.
|
||||
|The fingerprint of the key we want to rotate.
|
||||
|The id of the new KMS key (e.g. Resource Name)."""
|
||||
)
|
||||
def rotate_kms_node_key(fingerprint: String, newKmsKeyId: String): PublicKey = {
|
||||
|
||||
val owner = instance.id.member
|
||||
|
||||
val currentKey = findPublicKey(fingerprint, instance.topology, owner)
|
||||
val newKey = currentKey.purpose match {
|
||||
case KeyPurpose.Signing => instance.keys.secret.register_kms_signing_key(newKmsKeyId)
|
||||
case KeyPurpose.Encryption => instance.keys.secret.register_kms_encryption_key(newKmsKeyId)
|
||||
}
|
||||
|
||||
// Rotate the key for the node in the topology management
|
||||
instance.topology.owner_to_key_mappings.rotate_key(
|
||||
instance,
|
||||
owner,
|
||||
currentKey,
|
||||
newKey,
|
||||
)
|
||||
newKey
|
||||
}
|
||||
|
||||
@Help.Summary("Rotate a node's public/private key pair")
|
||||
@Help.Description(
|
||||
"""Rotates an existing encryption or signing key. NOTE: A namespace root or intermediate
|
||||
signing key CANNOT be rotated by this command.
|
||||
|The fingerprint of the key we want to rotate."""
|
||||
)
|
||||
def rotate_node_key(fingerprint: String, name: Option[String] = None): PublicKey = {
|
||||
val owner = instance.id.member
|
||||
|
||||
val currentKey = findPublicKey(fingerprint, instance.topology, owner)
|
||||
|
||||
val newKey = name match {
|
||||
case Some(_) => regenerateKey(currentKey, name)
|
||||
case None =>
|
||||
regenerateKey(
|
||||
currentKey,
|
||||
generateNewNameForRotatedKey(fingerprint, consoleEnvironment.environment.clock),
|
||||
)
|
||||
}
|
||||
|
||||
// Rotate the key for the node in the topology management
|
||||
instance.topology.owner_to_key_mappings.rotate_key(
|
||||
instance,
|
||||
owner,
|
||||
currentKey,
|
||||
newKey,
|
||||
)
|
||||
newKey
|
||||
}
|
||||
|
||||
@Help.Summary("Rotate the node's public/private key pairs")
|
||||
@Help.Description(
|
||||
"""
|
||||
|For a participant node it rotates the signing and encryption key pair.
|
||||
|For a domain or domain manager node it rotates the signing key pair as those nodes do not have an encryption key pair.
|
||||
|For a sequencer or mediator node use `rotate_node_keys` with a domain manager reference as an argument.
|
||||
|NOTE: Namespace root or intermediate signing keys are NOT rotated by this command."""
|
||||
)
|
||||
def rotate_node_keys(): Unit = {
|
||||
|
||||
val owner = instance.id.member
|
||||
|
||||
// Find the current keys
|
||||
val currentKeys = findPublicKeys(instance.topology, owner)
|
||||
|
||||
currentKeys.foreach { currentKey =>
|
||||
val newKey =
|
||||
regenerateKey(
|
||||
currentKey,
|
||||
generateNewNameForRotatedKey(
|
||||
currentKey.fingerprint.unwrap,
|
||||
consoleEnvironment.environment.clock,
|
||||
),
|
||||
)
|
||||
|
||||
// Rotate the key for the node in the topology management
|
||||
instance.topology.owner_to_key_mappings.rotate_key(
|
||||
instance,
|
||||
owner,
|
||||
currentKey,
|
||||
newKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper to find public keys for topology/x shared between community and enterprise
|
||||
*/
|
||||
protected def findPublicKeys(
|
||||
topologyAdmin: TopologyAdministrationGroupCommon,
|
||||
owner: Member,
|
||||
): Seq[PublicKey] =
|
||||
topologyAdmin match {
|
||||
case t: TopologyAdministrationGroup =>
|
||||
t.owner_to_key_mappings
|
||||
.list(
|
||||
filterStore = AuthorizedStore.filterName,
|
||||
filterKeyOwnerUid = owner.filterString,
|
||||
filterKeyOwnerType = Some(owner.code),
|
||||
)
|
||||
.map(_.item.key)
|
||||
case tx: TopologyAdministrationGroupX =>
|
||||
tx.owner_to_key_mappings
|
||||
.list(
|
||||
filterStore = AuthorizedStore.filterName,
|
||||
filterKeyOwnerUid = owner.filterString,
|
||||
filterKeyOwnerType = Some(owner.code),
|
||||
)
|
||||
.flatMap(_.item.keys)
|
||||
case _ =>
|
||||
throw new IllegalStateException(
|
||||
"Impossible to encounter topology admin group besides X and non-X"
|
||||
)
|
||||
}
|
||||
|
||||
/** Helper to name new keys generated during a rotation with a ...-rotated-<timestamp> tag to better identify
|
||||
* the new keys after a rotation
|
||||
*/
|
||||
protected def generateNewNameForRotatedKey(
|
||||
currentKeyId: String,
|
||||
clock: Clock,
|
||||
): Option[String] = {
|
||||
val keyName = instance.keys.secret
|
||||
.list()
|
||||
.find(_.publicKey.fingerprint.unwrap == currentKeyId)
|
||||
.flatMap(_.name)
|
||||
|
||||
val rotatedKeyRegExp = "(.*-rotated).*".r
|
||||
|
||||
keyName.map(_.unwrap) match {
|
||||
case Some(rotatedKeyRegExp(currentName)) =>
|
||||
Some(s"$currentName-${clock.now.show}")
|
||||
case Some(currentName) =>
|
||||
Some(s"$currentName-rotated-${clock.now.show}")
|
||||
case None => None
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Change the wrapper key for encrypted private keys store")
|
||||
@Help.Description(
|
||||
"""Change the wrapper key (e.g. AWS KMS key) being used to encrypt the private keys in the store.
|
||||
|newWrapperKeyId: The optional new wrapper key id to be used. If the wrapper key id is empty Canton will generate a new key based on the current configuration."""
|
||||
)
|
||||
def rotate_wrapper_key(
|
||||
newWrapperKeyId: String = ""
|
||||
): Unit = {
|
||||
consoleEnvironment.run {
|
||||
adminCommand(VaultAdminCommands.RotateWrapperKey(newWrapperKeyId))
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Get the wrapper key id that is used for the encrypted private keys store")
|
||||
def get_wrapper_key_id(): String = {
|
||||
consoleEnvironment.run {
|
||||
adminCommand(VaultAdminCommands.GetWrapperKeyId())
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Upload (load and import) a key pair from file")
|
||||
def upload(filename: String, name: Option[String]): Unit = {
|
||||
val keyPair = BinaryFileUtil.tryReadByteStringFromFile(filename)
|
||||
upload(keyPair, name)
|
||||
}
|
||||
|
||||
@Help.Summary("Upload a key pair")
|
||||
def upload(
|
||||
pairBytes: ByteString,
|
||||
name: Option[String],
|
||||
): Unit =
|
||||
consoleEnvironment.run {
|
||||
adminCommand(
|
||||
VaultAdminCommands.ImportKeyPair(pairBytes, name)
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(i13613): Remove feature flag
|
||||
@Help.Summary("Download key pair", FeatureFlag.Preview)
|
||||
@Help.Description(
|
||||
"""Download the key pair with the private and public key in its binary representation.
|
||||
|fingerprint: The identifier of the key pair to download
|
||||
|protocolVersion: The (optional) protocol version that defines the serialization of the key pair"""
|
||||
)
|
||||
def download(
|
||||
fingerprint: Fingerprint,
|
||||
protocolVersion: ProtocolVersion = ProtocolVersion.latest,
|
||||
): ByteString = {
|
||||
check(FeatureFlag.Preview) {
|
||||
consoleEnvironment.run {
|
||||
adminCommand(
|
||||
VaultAdminCommands.ExportKeyPair(fingerprint, protocolVersion)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected def writeToFile(outputFile: String, bytes: ByteString): Unit = {
|
||||
val file = new File(outputFile)
|
||||
file.createNewFile()
|
||||
// only current user has permissions with the file
|
||||
try {
|
||||
Files.setPosixFilePermissions(file.toPath, Set(OWNER_READ, OWNER_WRITE).asJava)
|
||||
} catch {
|
||||
// the above will throw on non-posix systems such as windows
|
||||
case _: UnsupportedOperationException =>
|
||||
}
|
||||
BinaryFileUtil.writeByteStringToFile(outputFile, bytes)
|
||||
}
|
||||
|
||||
@Help.Summary("Download key pair and save it to a file")
|
||||
def download_to(
|
||||
fingerprint: Fingerprint,
|
||||
outputFile: String,
|
||||
protocolVersion: ProtocolVersion = ProtocolVersion.latest,
|
||||
): Unit = {
|
||||
writeToFile(outputFile, download(fingerprint, protocolVersion))
|
||||
}
|
||||
|
||||
@Help.Summary("Delete private key")
|
||||
def delete(fingerprint: Fingerprint, force: Boolean = false): Unit = {
|
||||
def deleteKey(): Unit =
|
||||
consoleEnvironment.run {
|
||||
adminCommand(
|
||||
VaultAdminCommands.DeleteKeyPair(fingerprint)
|
||||
)
|
||||
}
|
||||
|
||||
if (force)
|
||||
deleteKey()
|
||||
else {
|
||||
println(
|
||||
s"Are you sure you want to delete the private key with fingerprint $fingerprint? yes/no"
|
||||
)
|
||||
println(s"This action is irreversible and can have undesired effects if done carelessly.")
|
||||
print("> ")
|
||||
val answer = Option(scala.io.StdIn.readLine())
|
||||
if (answer.exists(_.toLowerCase == "yes")) deleteKey()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PublicKeyAdministration(
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
) extends Helpful {
|
||||
|
||||
import runner.*
|
||||
|
||||
private def defaultLimit: PositiveInt =
|
||||
consoleEnvironment.environment.config.parameters.console.defaultLimit
|
||||
|
||||
@Help.Summary("Upload public key")
|
||||
@Help.Description(
|
||||
"""Import a public key and store it together with a name used to provide some context to that key."""
|
||||
)
|
||||
def upload(keyBytes: ByteString, name: Option[String]): Fingerprint = consoleEnvironment.run {
|
||||
adminCommand(
|
||||
VaultAdminCommands.ImportPublicKey(keyBytes, name)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("Upload public key")
|
||||
@Help.Summary(
|
||||
"Load a public key from a file and store it together with a name used to provide some context to that key."
|
||||
)
|
||||
def upload(filename: String, name: Option[String]): Fingerprint = consoleEnvironment.run {
|
||||
BinaryFileUtil.readByteStringFromFile(filename) match {
|
||||
case Right(bytes) => adminCommand(VaultAdminCommands.ImportPublicKey(bytes, name))
|
||||
case Left(err) => throw new IllegalArgumentException(err)
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Download public key")
|
||||
def download(
|
||||
fingerprint: Fingerprint,
|
||||
protocolVersion: ProtocolVersion = ProtocolVersion.latest,
|
||||
): ByteString = {
|
||||
val keys = list(fingerprint.unwrap)
|
||||
if (keys.sizeCompare(1) == 0) { // vector doesn't like matching on Nil
|
||||
val key = keys.headOption.getOrElse(sys.error("no key"))
|
||||
key.publicKey.toByteString(protocolVersion)
|
||||
} else {
|
||||
if (keys.isEmpty) throw new IllegalArgumentException(s"no key found for [$fingerprint]")
|
||||
else
|
||||
throw new IllegalArgumentException(
|
||||
s"found multiple results for [$fingerprint]: ${keys.map(_.publicKey.fingerprint)}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Download public key and save it to a file")
|
||||
def download_to(
|
||||
fingerprint: Fingerprint,
|
||||
outputFile: String,
|
||||
protocolVersion: ProtocolVersion = ProtocolVersion.latest,
|
||||
): Unit = {
|
||||
BinaryFileUtil.writeByteStringToFile(
|
||||
outputFile,
|
||||
download(fingerprint, protocolVersion),
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("List public keys in registry")
|
||||
@Help.Description("""Returns all public keys that have been added to the key registry.
|
||||
Optional arguments can be used for filtering.""")
|
||||
def list(filterFingerprint: String = "", filterContext: String = ""): Seq[PublicKeyWithName] =
|
||||
consoleEnvironment.run {
|
||||
adminCommand(VaultAdminCommands.ListPublicKeys(filterFingerprint, filterContext))
|
||||
}
|
||||
|
||||
@Help.Summary("List active owners with keys for given search arguments.")
|
||||
@Help.Description("""This command allows deep inspection of the topology state.
|
||||
|The response includes the public keys.
|
||||
|Optional filterKeyOwnerType type can be 'ParticipantId.Code' , 'MediatorId.Code','SequencerId.Code', 'DomainTopologyManagerId.Code'.
|
||||
|""")
|
||||
def list_owners(
|
||||
filterKeyOwnerUid: String = "",
|
||||
filterKeyOwnerType: Option[MemberCode] = None,
|
||||
filterDomain: String = "",
|
||||
asOf: Option[Instant] = None,
|
||||
limit: PositiveInt = defaultLimit,
|
||||
): Seq[ListKeyOwnersResult] = consoleEnvironment.run {
|
||||
adminCommand(
|
||||
TopologyAdminCommands.Aggregation
|
||||
.ListKeyOwners(filterDomain, filterKeyOwnerType, filterKeyOwnerUid, asOf, limit)
|
||||
)
|
||||
}
|
||||
|
||||
@Help.Summary("List keys for given keyOwner.")
|
||||
@Help.Description(
|
||||
"""This command is a convenience wrapper for `list_key_owners`, taking an explicit keyOwner as search argument.
|
||||
|The response includes the public keys."""
|
||||
)
|
||||
def list_by_owner(
|
||||
keyOwner: Member,
|
||||
filterDomain: String = "",
|
||||
asOf: Option[Instant] = None,
|
||||
limit: PositiveInt = defaultLimit,
|
||||
): Seq[ListKeyOwnersResult] = consoleEnvironment.run {
|
||||
adminCommand(
|
||||
TopologyAdminCommands.Aggregation.ListKeyOwners(
|
||||
filterDomain = filterDomain,
|
||||
filterKeyOwnerType = Some(keyOwner.code),
|
||||
filterKeyOwnerUid = keyOwner.uid.toProtoPrimitive,
|
||||
asOf,
|
||||
limit,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class KeyAdministrationGroup(
|
||||
instance: InstanceReferenceCommon,
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
) extends Helpful {
|
||||
|
||||
private lazy val publicAdmin =
|
||||
new PublicKeyAdministration(runner, consoleEnvironment)
|
||||
private lazy val secretAdmin =
|
||||
new SecretKeyAdministration(instance, runner, consoleEnvironment, loggerFactory)
|
||||
|
||||
@Help.Summary("Manage public keys")
|
||||
@Help.Group("Public keys")
|
||||
def public: PublicKeyAdministration = publicAdmin
|
||||
|
||||
@Help.Summary("Manage secret keys")
|
||||
@Help.Group("Secret keys")
|
||||
def secret: SecretKeyAdministration = secretAdmin
|
||||
|
||||
}
|
||||
|
||||
class LocalSecretKeyAdministration(
|
||||
instance: InstanceReferenceCommon,
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
crypto: => Crypto,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
)(implicit executionContext: ExecutionContext)
|
||||
extends SecretKeyAdministration(instance, runner, consoleEnvironment, loggerFactory) {
|
||||
|
||||
private def run[V](eitherT: EitherT[Future, String, V], action: String): V = {
|
||||
import TraceContext.Implicits.Empty.*
|
||||
consoleEnvironment.environment.config.parameters.timeouts.processing.default
|
||||
.await(action)(eitherT.value) match {
|
||||
case Left(error) =>
|
||||
throw new IllegalArgumentException(s"Problem while $action. Error: $error")
|
||||
case Right(value) => value
|
||||
}
|
||||
}
|
||||
|
||||
@Help.Summary("Download key pair")
|
||||
override def download(
|
||||
fingerprint: Fingerprint,
|
||||
protocolVersion: ProtocolVersion = ProtocolVersion.latest,
|
||||
): ByteString =
|
||||
TraceContext.withNewTraceContext { implicit traceContext =>
|
||||
val cmd = for {
|
||||
cryptoPrivateStore <- crypto.cryptoPrivateStore.toExtended
|
||||
.toRight(
|
||||
"The selected crypto provider does not support exporting of private keys."
|
||||
)
|
||||
.toEitherT[Future]
|
||||
privateKey <- cryptoPrivateStore
|
||||
.exportPrivateKey(fingerprint)
|
||||
.leftMap(_.toString)
|
||||
.subflatMap(_.toRight(s"no private key found for [$fingerprint]"))
|
||||
.leftMap(err => s"Error retrieving private key [$fingerprint] $err")
|
||||
publicKey <- crypto.cryptoPublicStore
|
||||
.publicKey(fingerprint)
|
||||
.leftMap(_.toString)
|
||||
.subflatMap(_.toRight(s"no public key found for [$fingerprint]"))
|
||||
.leftMap(err => s"Error retrieving public key [$fingerprint] $err")
|
||||
keyPair: CryptoKeyPair[PublicKey, PrivateKey] = (publicKey, privateKey) match {
|
||||
case (pub: SigningPublicKey, pkey: SigningPrivateKey) =>
|
||||
new SigningKeyPair(pub, pkey)
|
||||
case (pub: EncryptionPublicKey, pkey: EncryptionPrivateKey) =>
|
||||
new EncryptionKeyPair(pub, pkey)
|
||||
case _ => sys.error("public and private keys must have same purpose")
|
||||
}
|
||||
keyPairBytes = keyPair.toByteString(protocolVersion)
|
||||
} yield keyPairBytes
|
||||
run(cmd, "exporting key pair")
|
||||
}
|
||||
|
||||
@Help.Summary("Download key pair and save it to a file")
|
||||
override def download_to(
|
||||
fingerprint: Fingerprint,
|
||||
outputFile: String,
|
||||
protocolVersion: ProtocolVersion = ProtocolVersion.latest,
|
||||
): Unit =
|
||||
run(
|
||||
EitherT.rightT(writeToFile(outputFile, download(fingerprint, protocolVersion))),
|
||||
"saving key pair to file",
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
class LocalKeyAdministrationGroup(
|
||||
instance: InstanceReferenceCommon,
|
||||
runner: AdminCommandRunner,
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
crypto: => Crypto,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
)(implicit executionContext: ExecutionContext)
|
||||
extends KeyAdministrationGroup(instance, runner, consoleEnvironment, loggerFactory) {
|
||||
|
||||
private lazy val localSecretAdmin: LocalSecretKeyAdministration =
|
||||
new LocalSecretKeyAdministration(instance, runner, consoleEnvironment, crypto, loggerFactory)
|
||||
|
||||
@Help.Summary("Manage secret keys")
|
||||
@Help.Group("Secret keys")
|
||||
override def secret: LocalSecretKeyAdministration = localSecretAdmin
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.functorFilter.*
|
||||
import com.digitalasset.canton.DiscardOps
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.logging.ErrorLoggingContext
|
||||
|
||||
package object commands {
|
||||
|
||||
/** Runs every body, even if some of them fail with a `CommandExecutionFailedException`.
|
||||
* Succeeds, if all bodies succeed.
|
||||
* If some body throws a `Throwable` other than `CommandExecutionFailedException`, the execution terminates immediately with that exception.
|
||||
* If some body throws a `CommandExecutionFailedException`, subsequent bodies are still executed and afterwards the
|
||||
* methods throws a `CommandExecutionFailedException`, preferring `CantonInternalErrors` over `CommandFailure`.
|
||||
*/
|
||||
private[commands] def runEvery[A](bodies: Seq[() => Unit]): Unit = {
|
||||
val exceptions = bodies.mapFilter(body =>
|
||||
try {
|
||||
body()
|
||||
None
|
||||
} catch {
|
||||
case e: CommandFailure => Some(e)
|
||||
case e: CantonInternalError => Some(e)
|
||||
}
|
||||
)
|
||||
// It is ok to discard all except one exceptions, because:
|
||||
// - The exceptions do not have meaningful messages. Error messages are logged instead.
|
||||
// - The exception have all the same stack trace.
|
||||
exceptions.collectFirst { case e: CantonInternalError => throw e }.discard
|
||||
exceptions.headOption.foreach(throw _)
|
||||
}
|
||||
|
||||
private[commands] def timestampFromInstant(
|
||||
instant: java.time.Instant
|
||||
)(implicit loggingContext: ErrorLoggingContext): CantonTimestamp =
|
||||
CantonTimestamp.fromInstant(instant).valueOr { err =>
|
||||
loggingContext.logger.error(err)(loggingContext.traceContext)
|
||||
throw new CommandFailure()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton
|
||||
|
||||
import com.digitalasset.canton.console.CommandErrors.GenericCommandError
|
||||
|
||||
/** General `console` utilities
|
||||
*/
|
||||
package object console {
|
||||
|
||||
/** Turn a either into a command result.
|
||||
* Left is considered an error, Right is successful.
|
||||
*/
|
||||
implicit class EitherToCommandResultExtensions[A, B](either: Either[A, B]) {
|
||||
def toResult(errorDescription: A => String): ConsoleCommandResult[B] =
|
||||
either.fold[ConsoleCommandResult[B]](
|
||||
err => GenericCommandError(errorDescription(err)),
|
||||
CommandSuccessful[B],
|
||||
)
|
||||
|
||||
def toResult[Result](
|
||||
errorDescription: A => String,
|
||||
resultMap: B => Result,
|
||||
): ConsoleCommandResult[Result] =
|
||||
either.fold[ConsoleCommandResult[Result]](
|
||||
err => GenericCommandError(errorDescription(err)),
|
||||
result => CommandSuccessful(resultMap(result)),
|
||||
)
|
||||
}
|
||||
|
||||
/** Turn an either where Left is a error message into a ConsoleCommandResult.
|
||||
*/
|
||||
implicit class StringErrorEitherToCommandResultExtensions[A](either: Either[String, A]) {
|
||||
def toResult: ConsoleCommandResult[A] =
|
||||
either.fold[ConsoleCommandResult[A]](GenericCommandError, CommandSuccessful[A])
|
||||
}
|
||||
|
||||
/** Strip the Object suffix from the name of the provided class
|
||||
*/
|
||||
def objectClassNameWithoutSuffix(c: Class[_]): String =
|
||||
c.getName.stripSuffix("$").replace('$', '.')
|
||||
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.environment
|
||||
|
||||
import com.digitalasset.canton.admin.api.client.data.CommunityCantonStatus
|
||||
import com.digitalasset.canton.config.{CantonCommunityConfig, TestingConfigInternal}
|
||||
import com.digitalasset.canton.console.{
|
||||
CantonHealthAdministration,
|
||||
CommunityCantonHealthAdministration,
|
||||
CommunityHealthDumpGenerator,
|
||||
CommunityLocalDomainReference,
|
||||
CommunityRemoteDomainReference,
|
||||
ConsoleEnvironment,
|
||||
ConsoleEnvironmentBinding,
|
||||
ConsoleGrpcAdminCommandRunner,
|
||||
ConsoleOutput,
|
||||
DomainReference,
|
||||
FeatureFlag,
|
||||
GrpcAdminCommandRunner,
|
||||
HealthDumpGenerator,
|
||||
Help,
|
||||
LocalDomainReference,
|
||||
LocalInstanceReferenceCommon,
|
||||
LocalParticipantReference,
|
||||
NodeReferences,
|
||||
StandardConsoleOutput,
|
||||
}
|
||||
import com.digitalasset.canton.domain.DomainNodeBootstrap
|
||||
import com.digitalasset.canton.logging.NamedLoggerFactory
|
||||
import com.digitalasset.canton.participant.{ParticipantNodeBootstrap, ParticipantNodeBootstrapX}
|
||||
import com.digitalasset.canton.resource.{CommunityDbMigrationsFactory, DbMigrationsFactory}
|
||||
|
||||
class CommunityEnvironment(
|
||||
override val config: CantonCommunityConfig,
|
||||
override val testingConfig: TestingConfigInternal,
|
||||
override val loggerFactory: NamedLoggerFactory,
|
||||
) extends Environment {
|
||||
|
||||
override type Config = CantonCommunityConfig
|
||||
|
||||
override protected val participantNodeFactory
|
||||
: ParticipantNodeBootstrap.Factory[Config#ParticipantConfigType, ParticipantNodeBootstrap] =
|
||||
ParticipantNodeBootstrap.CommunityParticipantFactory
|
||||
override protected val participantNodeFactoryX
|
||||
: ParticipantNodeBootstrap.Factory[Config#ParticipantConfigType, ParticipantNodeBootstrapX] =
|
||||
ParticipantNodeBootstrapX.CommunityParticipantFactory
|
||||
|
||||
override protected val domainFactory: DomainNodeBootstrap.Factory[Config#DomainConfigType] =
|
||||
DomainNodeBootstrap.CommunityDomainFactory
|
||||
override type Console = CommunityConsoleEnvironment
|
||||
|
||||
override protected def _createConsole(
|
||||
consoleOutput: ConsoleOutput,
|
||||
createAdminCommandRunner: ConsoleEnvironment => ConsoleGrpcAdminCommandRunner,
|
||||
): CommunityConsoleEnvironment =
|
||||
new CommunityConsoleEnvironment(this, consoleOutput, createAdminCommandRunner)
|
||||
|
||||
override protected lazy val migrationsFactory: DbMigrationsFactory =
|
||||
new CommunityDbMigrationsFactory(loggerFactory)
|
||||
|
||||
override def isEnterprise: Boolean = false
|
||||
|
||||
def createHealthDumpGenerator(
|
||||
commandRunner: GrpcAdminCommandRunner
|
||||
): HealthDumpGenerator[CommunityCantonStatus] = {
|
||||
new CommunityHealthDumpGenerator(this, commandRunner)
|
||||
}
|
||||
}
|
||||
|
||||
object CommunityEnvironmentFactory extends EnvironmentFactory[CommunityEnvironment] {
|
||||
override def create(
|
||||
config: CantonCommunityConfig,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
testingConfigInternal: TestingConfigInternal,
|
||||
): CommunityEnvironment =
|
||||
new CommunityEnvironment(config, testingConfigInternal, loggerFactory)
|
||||
}
|
||||
|
||||
class CommunityConsoleEnvironment(
|
||||
val environment: CommunityEnvironment,
|
||||
val consoleOutput: ConsoleOutput = StandardConsoleOutput,
|
||||
protected val createAdminCommandRunner: ConsoleEnvironment => ConsoleGrpcAdminCommandRunner =
|
||||
new ConsoleGrpcAdminCommandRunner(_),
|
||||
) extends ConsoleEnvironment {
|
||||
override type Env = CommunityEnvironment
|
||||
override type DomainLocalRef = CommunityLocalDomainReference
|
||||
override type DomainRemoteRef = CommunityRemoteDomainReference
|
||||
override type Status = CommunityCantonStatus
|
||||
|
||||
private lazy val health_ = new CommunityCantonHealthAdministration(this)
|
||||
override protected val consoleEnvironmentBindings = new ConsoleEnvironmentBinding()
|
||||
|
||||
@Help.Summary("Environment health inspection")
|
||||
@Help.Group("Health")
|
||||
override def health: CantonHealthAdministration[Status] =
|
||||
health_
|
||||
|
||||
override def startupOrderPrecedence(instance: LocalInstanceReferenceCommon): Int =
|
||||
instance match {
|
||||
case _: LocalDomainReference => 1
|
||||
case _: LocalParticipantReference => 2
|
||||
case _ => 3
|
||||
}
|
||||
|
||||
override protected def createDomainReference(name: String): DomainLocalRef =
|
||||
new CommunityLocalDomainReference(this, name, environment.executionContext)
|
||||
|
||||
override protected def createRemoteDomainReference(name: String): DomainRemoteRef =
|
||||
new CommunityRemoteDomainReference(this, name)
|
||||
|
||||
override protected def domainsTopLevelValue(
|
||||
h: TopLevelValue.Partial,
|
||||
domains: NodeReferences[
|
||||
DomainReference,
|
||||
CommunityRemoteDomainReference,
|
||||
CommunityLocalDomainReference,
|
||||
],
|
||||
): TopLevelValue[
|
||||
NodeReferences[DomainReference, CommunityRemoteDomainReference, CommunityLocalDomainReference]
|
||||
] =
|
||||
h(domains)
|
||||
|
||||
override protected def localDomainTopLevelValue(
|
||||
h: TopLevelValue.Partial,
|
||||
d: CommunityLocalDomainReference,
|
||||
): TopLevelValue[CommunityLocalDomainReference] =
|
||||
h(d)
|
||||
|
||||
override protected def remoteDomainTopLevelValue(
|
||||
h: TopLevelValue.Partial,
|
||||
d: CommunityRemoteDomainReference,
|
||||
): TopLevelValue[CommunityRemoteDomainReference] =
|
||||
h(d)
|
||||
|
||||
override protected def localDomainHelpItems(
|
||||
scope: Set[FeatureFlag],
|
||||
localDomain: CommunityLocalDomainReference,
|
||||
): Seq[Help.Item] =
|
||||
Help.getItems(localDomain, baseTopic = Seq("$domain"), scope = scope)
|
||||
|
||||
override protected def remoteDomainHelpItems(
|
||||
scope: Set[FeatureFlag],
|
||||
remoteDomain: CommunityRemoteDomainReference,
|
||||
): Seq[Help.Item] =
|
||||
Help.getItems(remoteDomain, baseTopic = Seq("$domain"), scope = scope)
|
||||
}
|
@ -0,0 +1,627 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.environment
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.instances.option.*
|
||||
import cats.syntax.apply.*
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.foldable.*
|
||||
import cats.syntax.traverse.*
|
||||
import com.daml.grpc.adapter.ExecutionSequencerFactory
|
||||
import com.digitalasset.canton.concurrent.*
|
||||
import com.digitalasset.canton.config.*
|
||||
import com.digitalasset.canton.console.{
|
||||
ConsoleEnvironment,
|
||||
ConsoleGrpcAdminCommandRunner,
|
||||
ConsoleOutput,
|
||||
GrpcAdminCommandRunner,
|
||||
HealthDumpGenerator,
|
||||
StandardConsoleOutput,
|
||||
}
|
||||
import com.digitalasset.canton.data.CantonTimestamp
|
||||
import com.digitalasset.canton.domain.DomainNodeBootstrap
|
||||
import com.digitalasset.canton.environment.CantonNodeBootstrap.HealthDumpFunction
|
||||
import com.digitalasset.canton.environment.Environment.*
|
||||
import com.digitalasset.canton.environment.ParticipantNodes.{ParticipantNodesOld, ParticipantNodesX}
|
||||
import com.digitalasset.canton.health.{HealthCheck, HealthServer}
|
||||
import com.digitalasset.canton.lifecycle.Lifecycle
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.metrics.MetricsConfig.Prometheus
|
||||
import com.digitalasset.canton.metrics.MetricsFactory
|
||||
import com.digitalasset.canton.participant.domain.DomainConnectionConfig
|
||||
import com.digitalasset.canton.participant.{
|
||||
ParticipantNode,
|
||||
ParticipantNodeBootstrap,
|
||||
ParticipantNodeBootstrapCommon,
|
||||
ParticipantNodeBootstrapX,
|
||||
ParticipantNodeCommon,
|
||||
}
|
||||
import com.digitalasset.canton.resource.DbMigrationsFactory
|
||||
import com.digitalasset.canton.sequencing.SequencerConnections
|
||||
import com.digitalasset.canton.telemetry.{ConfiguredOpenTelemetry, OpenTelemetryFactory}
|
||||
import com.digitalasset.canton.time.EnrichedDurations.*
|
||||
import com.digitalasset.canton.time.*
|
||||
import com.digitalasset.canton.tracing.TraceContext.withNewTraceContext
|
||||
import com.digitalasset.canton.tracing.{NoTracing, TraceContext, TracerProvider}
|
||||
import com.digitalasset.canton.util.FutureInstances.parallelFuture
|
||||
import com.digitalasset.canton.util.{MonadUtil, PekkoUtil, SingleUseCell}
|
||||
import com.digitalasset.canton.{DiscardOps, DomainAlias}
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import io.circe.Encoder
|
||||
import io.opentelemetry.api.trace.Tracer
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.concurrent.{Future, blocking}
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
/** Holds all significant resources held by this process.
|
||||
*/
|
||||
trait Environment extends NamedLogging with AutoCloseable with NoTracing {
|
||||
|
||||
type Config <: CantonConfig
|
||||
type Console <: ConsoleEnvironment
|
||||
|
||||
val config: Config
|
||||
val testingConfig: TestingConfigInternal
|
||||
|
||||
val loggerFactory: NamedLoggerFactory
|
||||
|
||||
lazy val configuredOpenTelemetry: ConfiguredOpenTelemetry = {
|
||||
val isPrometheusEnabled = config.monitoring.metrics.reporters.exists {
|
||||
case _: Prometheus => true
|
||||
case _ => false
|
||||
}
|
||||
OpenTelemetryFactory.initializeOpenTelemetry(
|
||||
testingConfig.initializeGlobalOpenTelemetry,
|
||||
isPrometheusEnabled,
|
||||
config.monitoring.tracing.tracer,
|
||||
config.monitoring.metrics.histograms,
|
||||
loggerFactory,
|
||||
)
|
||||
}
|
||||
|
||||
// public for buildDocs task to be able to construct a fake participant and domain to document available metrics via reflection
|
||||
lazy val metricsFactory: MetricsFactory =
|
||||
MetricsFactory.forConfig(
|
||||
config.monitoring.metrics,
|
||||
configuredOpenTelemetry.openTelemetry,
|
||||
testingConfig.metricsFactoryType,
|
||||
)
|
||||
protected def participantNodeFactory
|
||||
: ParticipantNodeBootstrap.Factory[Config#ParticipantConfigType, ParticipantNodeBootstrap]
|
||||
protected def participantNodeFactoryX
|
||||
: ParticipantNodeBootstrap.Factory[Config#ParticipantConfigType, ParticipantNodeBootstrapX]
|
||||
protected def domainFactory: DomainNodeBootstrap.Factory[Config#DomainConfigType]
|
||||
protected def migrationsFactory: DbMigrationsFactory
|
||||
|
||||
def isEnterprise: Boolean
|
||||
|
||||
def createConsole(
|
||||
consoleOutput: ConsoleOutput = StandardConsoleOutput,
|
||||
createAdminCommandRunner: ConsoleEnvironment => ConsoleGrpcAdminCommandRunner =
|
||||
new ConsoleGrpcAdminCommandRunner(_),
|
||||
): Console = {
|
||||
val console = _createConsole(consoleOutput, createAdminCommandRunner)
|
||||
healthDumpGenerator
|
||||
.putIfAbsent(createHealthDumpGenerator(console.grpcAdminCommandRunner))
|
||||
.discard
|
||||
console
|
||||
}
|
||||
|
||||
protected def _createConsole(
|
||||
consoleOutput: ConsoleOutput = StandardConsoleOutput,
|
||||
createAdminCommandRunner: ConsoleEnvironment => ConsoleGrpcAdminCommandRunner =
|
||||
new ConsoleGrpcAdminCommandRunner(_),
|
||||
): Console
|
||||
|
||||
protected def createHealthDumpGenerator(
|
||||
commandRunner: GrpcAdminCommandRunner
|
||||
): HealthDumpGenerator[_]
|
||||
|
||||
/* We can't reliably use the health administration instance of the console because:
|
||||
* 1) it's tied to the console environment, which we don't have access to yet when the environment is instantiated
|
||||
* 2) there might never be a console environment when running in daemon mode
|
||||
* Therefore we create an immutable lazy value for the health administration that can be set either with the console
|
||||
* health admin when/if it gets created, or with a headless health admin, whichever comes first.
|
||||
*/
|
||||
private val healthDumpGenerator = new SingleUseCell[HealthDumpGenerator[_]]
|
||||
|
||||
// Function passed down to the node boostrap used to generate a health dump file
|
||||
val writeHealthDumpToFile: HealthDumpFunction = () =>
|
||||
Future {
|
||||
healthDumpGenerator
|
||||
.getOrElse {
|
||||
val tracerProvider =
|
||||
TracerProvider.Factory(configuredOpenTelemetry, "admin_command_runner")
|
||||
implicit val tracer: Tracer = tracerProvider.tracer
|
||||
|
||||
val commandRunner = new GrpcAdminCommandRunner(this, config.parameters.timeouts.console)
|
||||
val newGenerator = createHealthDumpGenerator(commandRunner)
|
||||
val previous = healthDumpGenerator.putIfAbsent(newGenerator)
|
||||
previous match {
|
||||
// If somehow the cell was set concurrently in the meantime, close the newly created command runner and use
|
||||
// the existing one
|
||||
case Some(value) =>
|
||||
commandRunner.close()
|
||||
value
|
||||
case None =>
|
||||
newGenerator
|
||||
}
|
||||
}
|
||||
.generateHealthDump(
|
||||
better.files.File.newTemporaryFile(
|
||||
prefix = "canton-remote-health-dump"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
installJavaUtilLoggingBridge()
|
||||
logger.debug(config.portDescription)
|
||||
|
||||
implicit val scheduler: ScheduledExecutorService =
|
||||
Threading.singleThreadScheduledExecutor(
|
||||
loggerFactory.threadName + "-env-scheduler",
|
||||
noTracingLogger,
|
||||
)
|
||||
|
||||
private val numThreads = Threading.detectNumberOfThreads(noTracingLogger)
|
||||
implicit val executionContext: ExecutionContextIdlenessExecutorService =
|
||||
Threading.newExecutionContext(
|
||||
loggerFactory.threadName + "-env-execution-context",
|
||||
noTracingLogger,
|
||||
Option.when(config.monitoring.metrics.reportExecutionContextMetrics)(
|
||||
metricsFactory.executionServiceMetrics
|
||||
),
|
||||
numThreads,
|
||||
)
|
||||
|
||||
private val deadlockConfig = config.monitoring.deadlockDetection
|
||||
protected def timeouts: ProcessingTimeout = config.parameters.timeouts.processing
|
||||
|
||||
protected val futureSupervisor =
|
||||
if (config.monitoring.logSlowFutures)
|
||||
new FutureSupervisor.Impl(timeouts.slowFutureWarn)
|
||||
else FutureSupervisor.Noop
|
||||
|
||||
private val monitorO = if (deadlockConfig.enabled) {
|
||||
val mon = new ExecutionContextMonitor(
|
||||
loggerFactory,
|
||||
deadlockConfig.interval.toInternal,
|
||||
deadlockConfig.warnInterval.toInternal,
|
||||
timeouts,
|
||||
)
|
||||
mon.monitor(executionContext)
|
||||
Some(mon)
|
||||
} else None
|
||||
|
||||
implicit val actorSystem: ActorSystem = PekkoUtil.createActorSystem(loggerFactory.threadName)
|
||||
|
||||
implicit val executionSequencerFactory: ExecutionSequencerFactory =
|
||||
PekkoUtil.createExecutionSequencerFactory(
|
||||
loggerFactory.threadName + "-admin-workflow-services",
|
||||
// don't log the number of threads twice, as we log it already when creating the first pool
|
||||
NamedLogging.noopNoTracingLogger,
|
||||
)
|
||||
|
||||
// additional closeables
|
||||
private val userCloseables = ListBuffer[AutoCloseable]()
|
||||
|
||||
/** Sim-clock if environment is using static time
|
||||
*/
|
||||
val simClock: Option[DelegatingSimClock] = config.parameters.clock match {
|
||||
case ClockConfig.SimClock =>
|
||||
logger.info("Starting environment with sim-clock")
|
||||
Some(
|
||||
new DelegatingSimClock(
|
||||
() =>
|
||||
runningNodes.map(_.clock).collect { case c: SimClock =>
|
||||
c
|
||||
},
|
||||
loggerFactory = loggerFactory,
|
||||
)
|
||||
)
|
||||
case ClockConfig.WallClock(_) => None
|
||||
case ClockConfig.RemoteClock(_) => None
|
||||
}
|
||||
|
||||
val clock: Clock = simClock.getOrElse(createClock(None))
|
||||
|
||||
protected def createClock(nodeTypeAndName: Option[(String, String)]): Clock = {
|
||||
val clockLoggerFactory = nodeTypeAndName.fold(loggerFactory) { case (nodeType, name) =>
|
||||
loggerFactory.append(nodeType, name)
|
||||
}
|
||||
config.parameters.clock match {
|
||||
case ClockConfig.SimClock =>
|
||||
val parent = simClock.getOrElse(sys.error("This should not happen"))
|
||||
val clock = new SimClock(
|
||||
parent.start,
|
||||
clockLoggerFactory,
|
||||
)
|
||||
clock.advanceTo(parent.now)
|
||||
clock
|
||||
case ClockConfig.RemoteClock(clientConfig) =>
|
||||
new RemoteClock(clientConfig, config.parameters.timeouts.processing, clockLoggerFactory)
|
||||
case ClockConfig.WallClock(skewW) =>
|
||||
val skewMs = skewW.asJava.toMillis
|
||||
val tickTock =
|
||||
if (skewMs == 0) TickTock.Native
|
||||
else new TickTock.RandomSkew(Math.min(skewMs, Int.MaxValue).toInt)
|
||||
new WallClock(timeouts, clockLoggerFactory, tickTock)
|
||||
}
|
||||
}
|
||||
|
||||
private val testingTimeService = new TestingTimeService(clock, () => simClocks)
|
||||
|
||||
protected lazy val healthCheck: Option[HealthCheck] = config.monitoring.health.map(config =>
|
||||
HealthCheck(config.check, metricsFactory.health, timeouts, loggerFactory)(this)
|
||||
)
|
||||
|
||||
private val healthServer =
|
||||
(healthCheck, config.monitoring.health).mapN { case (check, config) =>
|
||||
new HealthServer(check, config.server.address, config.server.port, timeouts, loggerFactory)
|
||||
}
|
||||
|
||||
private val envQueueSize = () => executionContext.queueSize
|
||||
metricsFactory.forEnv.registerExecutionContextQueueSize(envQueueSize)
|
||||
|
||||
lazy val domains =
|
||||
new DomainNodes(
|
||||
createDomain,
|
||||
migrationsFactory,
|
||||
timeouts,
|
||||
config.domainsByString,
|
||||
config.domainNodeParametersByString,
|
||||
loggerFactory,
|
||||
)
|
||||
lazy val participants =
|
||||
new ParticipantNodesOld[Config#ParticipantConfigType](
|
||||
createParticipant,
|
||||
migrationsFactory,
|
||||
timeouts,
|
||||
config.participantsByString,
|
||||
config.participantNodeParametersByString,
|
||||
loggerFactory,
|
||||
)
|
||||
lazy val participantsX =
|
||||
new ParticipantNodesX[Config#ParticipantConfigType](
|
||||
createParticipantX,
|
||||
migrationsFactory,
|
||||
timeouts,
|
||||
config.participantsByStringX,
|
||||
config.participantNodeParametersByString,
|
||||
loggerFactory,
|
||||
)
|
||||
|
||||
// convenient grouping of all node collections for performing operations
|
||||
// intentionally defined in the order we'd like to start them
|
||||
protected def allNodes: List[Nodes[CantonNode, CantonNodeBootstrap[CantonNode]]] =
|
||||
List(domains, participants, participantsX)
|
||||
private def runningNodes: Seq[CantonNodeBootstrap[CantonNode]] = allNodes.flatMap(_.running)
|
||||
|
||||
private def autoConnectLocalNodes(): Either[StartupError, Unit] = {
|
||||
// TODO(#14048) extend this to x-nodes
|
||||
val activeDomains = domains.running
|
||||
.filter(_.isActive)
|
||||
.filter(_.config.topology.open)
|
||||
def toDomainConfig(domain: DomainNodeBootstrap): Either[StartupError, DomainConnectionConfig] =
|
||||
(for {
|
||||
connection <- domain.config.sequencerConnectionConfig.toConnection
|
||||
name <- DomainAlias.create(domain.name.unwrap)
|
||||
sequencerConnections = SequencerConnections.single(connection)
|
||||
} yield DomainConnectionConfig(name, sequencerConnections)).leftMap(err =>
|
||||
StartFailed(domain.name.unwrap, s"Can not parse config for auto-connect: ${err}")
|
||||
)
|
||||
val connectParticipants =
|
||||
participants.running.filter(_.isActive).flatMap(x => x.getNode.map((x.name, _)).toList)
|
||||
def connect(
|
||||
name: String,
|
||||
node: ParticipantNode,
|
||||
configs: Seq[DomainConnectionConfig],
|
||||
): Either[StartupError, Unit] =
|
||||
configs.traverse_ { config =>
|
||||
val connectET =
|
||||
node
|
||||
.autoConnectLocalDomain(config)
|
||||
.leftMap(err => StartFailed(name, err.toString))
|
||||
.onShutdown(Left(StartFailed(name, "aborted due to shutdown")))
|
||||
this.config.parameters.timeouts.processing.unbounded
|
||||
.await("auto-connect to local domain")(connectET.value)
|
||||
}
|
||||
logger.info(s"Auto-connecting local participants ${connectParticipants
|
||||
.map(_._1.unwrap)} to local domains ${activeDomains.map(_.name.unwrap)}")
|
||||
activeDomains
|
||||
.traverse(toDomainConfig)
|
||||
.traverse_(config =>
|
||||
connectParticipants.traverse_ { case (name, node) => connect(name.unwrap, node, config) }
|
||||
)
|
||||
}
|
||||
|
||||
/** Try to startup all nodes in the configured environment and reconnect them to one another.
|
||||
* The first error will prevent further nodes from being started.
|
||||
* If an error is returned previously started nodes will not be stopped.
|
||||
*/
|
||||
def startAndReconnect(autoConnectLocal: Boolean): Either[StartupError, Unit] =
|
||||
withNewTraceContext { implicit traceContext =>
|
||||
if (config.parameters.manualStart) {
|
||||
logger.info("Manual start requested.")
|
||||
Right(())
|
||||
} else {
|
||||
logger.info("Automatically starting all instances")
|
||||
val startup = for {
|
||||
_ <- startAll()
|
||||
_ <- reconnectParticipants
|
||||
_ <- if (autoConnectLocal) autoConnectLocalNodes() else Right(())
|
||||
} yield writePortsFile()
|
||||
// log results
|
||||
startup
|
||||
.bimap(
|
||||
error => logger.error(s"Failed to start ${error.name}: ${error.message}"),
|
||||
_ => logger.info("Successfully started all nodes"),
|
||||
)
|
||||
.discard
|
||||
startup
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def writePortsFile()(implicit
|
||||
traceContext: TraceContext
|
||||
): Unit = {
|
||||
final case class ParticipantApis(ledgerApi: Int, adminApi: Int)
|
||||
config.parameters.portsFile.foreach { portsFile =>
|
||||
val items = participants.running.map { node =>
|
||||
(
|
||||
node.name.unwrap,
|
||||
ParticipantApis(node.config.ledgerApi.port.unwrap, node.config.adminApi.port.unwrap),
|
||||
)
|
||||
}.toMap
|
||||
import io.circe.syntax.*
|
||||
implicit val encoder: Encoder[ParticipantApis] =
|
||||
Encoder.forProduct2("ledgerApi", "adminApi") { apis =>
|
||||
(apis.ledgerApi, apis.adminApi)
|
||||
}
|
||||
val out = items.asJson.spaces2
|
||||
try {
|
||||
better.files.File(portsFile).overwrite(out)
|
||||
} catch {
|
||||
case NonFatal(ex) =>
|
||||
logger.warn(s"Failed to write to port file ${portsFile}. Will ignore the error", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def reconnectParticipants(implicit
|
||||
traceContext: TraceContext
|
||||
): Either[StartupError, Unit] = {
|
||||
def reconnect(
|
||||
instance: CantonNodeBootstrap[ParticipantNodeCommon] & ParticipantNodeBootstrapCommon
|
||||
): EitherT[Future, StartupError, Unit] = {
|
||||
instance.getNode match {
|
||||
case None =>
|
||||
// should not happen, but if it does, display at least a warning.
|
||||
if (instance.config.init.autoInit) {
|
||||
logger.error(
|
||||
s"Auto-initialisation failed or was too slow for ${instance.name}. Will not automatically re-connect to domains."
|
||||
)
|
||||
}
|
||||
EitherT.rightT(())
|
||||
case Some(node) =>
|
||||
node
|
||||
.reconnectDomainsIgnoreFailures()
|
||||
.leftMap(err => StartFailed(instance.name.unwrap, err.toString))
|
||||
.onShutdown(Left(StartFailed(instance.name.unwrap, "aborted due to shutdown")))
|
||||
|
||||
}
|
||||
}
|
||||
config.parameters.timeouts.processing.unbounded.await("reconnect-particiapnts")(
|
||||
MonadUtil
|
||||
.parTraverseWithLimit_(config.parameters.getStartupParallelism(numThreads))(
|
||||
(participants.running ++ participantsX.running)
|
||||
)(reconnect)
|
||||
.value
|
||||
)
|
||||
}
|
||||
|
||||
/** Return current time of environment
|
||||
*/
|
||||
def now: CantonTimestamp = clock.now
|
||||
|
||||
private def allNodesWithGroup = {
|
||||
allNodes.flatMap { nodeGroup =>
|
||||
nodeGroup.names().map(name => (name, nodeGroup))
|
||||
}
|
||||
}
|
||||
|
||||
/** Start all instances described in the configuration
|
||||
*/
|
||||
def startAll()(implicit traceContext: TraceContext): Either[StartupError, Unit] =
|
||||
startNodes(allNodesWithGroup)
|
||||
|
||||
def stopAll()(implicit traceContext: TraceContext): Either[ShutdownError, Unit] =
|
||||
stopNodes(allNodesWithGroup)
|
||||
|
||||
def startNodes(
|
||||
nodes: Seq[(String, Nodes[CantonNode, CantonNodeBootstrap[CantonNode]])]
|
||||
)(implicit traceContext: TraceContext): Either[StartupError, Unit] = {
|
||||
runOnNodesOrderedByStartupGroup(
|
||||
"startup-of-all-nodes",
|
||||
nodes,
|
||||
{ case (name, nodes) => nodes.start(name) },
|
||||
reverse = false,
|
||||
)
|
||||
}
|
||||
|
||||
def stopNodes(
|
||||
nodes: Seq[(String, Nodes[CantonNode, CantonNodeBootstrap[CantonNode]])]
|
||||
)(implicit traceContext: TraceContext): Either[ShutdownError, Unit] = {
|
||||
runOnNodesOrderedByStartupGroup(
|
||||
"stop-of-all-nodes",
|
||||
nodes,
|
||||
{ case (name, nodes) => nodes.stop(name) },
|
||||
reverse = true,
|
||||
)
|
||||
}
|
||||
|
||||
/** run some task on nodes ordered by their startup group
|
||||
*
|
||||
* @param reverse if true, then the order will be reverted (e.g. for stop)
|
||||
*/
|
||||
private def runOnNodesOrderedByStartupGroup[T, I](
|
||||
name: String,
|
||||
nodes: Seq[(String, Nodes[CantonNode, CantonNodeBootstrap[CantonNode]])],
|
||||
task: (String, Nodes[CantonNode, CantonNodeBootstrap[CantonNode]]) => EitherT[Future, T, I],
|
||||
reverse: Boolean,
|
||||
)(implicit traceContext: TraceContext): Either[T, Unit] = {
|
||||
config.parameters.timeouts.processing.unbounded.await(name)(
|
||||
MonadUtil
|
||||
.sequentialTraverse_(
|
||||
nodes
|
||||
// parallelize startup by groups (mediator / topology manager need the sequencer to run when we startup)
|
||||
// as otherwise, they start to emit a few warnings which are ugly
|
||||
.groupBy { case (_, group) => group.startUpGroup }
|
||||
.toList
|
||||
.sortBy { case (group, _) => if (reverse) -group else group }
|
||||
) { case (_, namesWithGroup) =>
|
||||
MonadUtil
|
||||
.parTraverseWithLimit_(config.parameters.getStartupParallelism(numThreads))(
|
||||
namesWithGroup.sortBy { case (name, _) =>
|
||||
name // sort by name to make the invocation order deterministic, hence also the result
|
||||
}
|
||||
) { case (name, nodes) => task(name, nodes) }
|
||||
}
|
||||
.value
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected def createParticipant(
|
||||
name: String,
|
||||
participantConfig: Config#ParticipantConfigType,
|
||||
): ParticipantNodeBootstrap = {
|
||||
participantNodeFactory
|
||||
.create(
|
||||
NodeFactoryArguments(
|
||||
name,
|
||||
participantConfig,
|
||||
config.participantNodeParametersByString(name),
|
||||
createClock(Some(ParticipantNodeBootstrap.LoggerFactoryKeyName -> name)),
|
||||
metricsFactory.forParticipant(name),
|
||||
testingConfig,
|
||||
futureSupervisor,
|
||||
loggerFactory.append(ParticipantNodeBootstrap.LoggerFactoryKeyName, name),
|
||||
writeHealthDumpToFile,
|
||||
configuredOpenTelemetry,
|
||||
),
|
||||
testingTimeService,
|
||||
)
|
||||
.valueOr(err => throw new RuntimeException(s"Failed to create participant bootstrap: $err"))
|
||||
}
|
||||
|
||||
protected def createParticipantX(
|
||||
name: String,
|
||||
participantConfig: Config#ParticipantConfigType,
|
||||
): ParticipantNodeBootstrapX = {
|
||||
participantNodeFactoryX
|
||||
.create(
|
||||
NodeFactoryArguments(
|
||||
name,
|
||||
participantConfig,
|
||||
// this is okay for x-nodes, as we've merged the two parameter sequences
|
||||
config.participantNodeParametersByString(name),
|
||||
createClock(Some(ParticipantNodeBootstrap.LoggerFactoryKeyName -> name)),
|
||||
metricsFactory.forParticipant(name),
|
||||
testingConfig,
|
||||
futureSupervisor,
|
||||
loggerFactory.append(ParticipantNodeBootstrap.LoggerFactoryKeyName, name),
|
||||
writeHealthDumpToFile,
|
||||
configuredOpenTelemetry,
|
||||
),
|
||||
testingTimeService,
|
||||
)
|
||||
.valueOr(err => throw new RuntimeException(s"Failed to create participant bootstrap: $err"))
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected def createDomain(
|
||||
name: String,
|
||||
domainConfig: config.DomainConfigType,
|
||||
): DomainNodeBootstrap =
|
||||
domainFactory
|
||||
.create(
|
||||
NodeFactoryArguments(
|
||||
name,
|
||||
domainConfig,
|
||||
config.domainNodeParametersByString(name),
|
||||
createClock(Some(DomainNodeBootstrap.LoggerFactoryKeyName -> name)),
|
||||
metricsFactory.forDomain(name),
|
||||
testingConfig,
|
||||
futureSupervisor,
|
||||
loggerFactory.append(DomainNodeBootstrap.LoggerFactoryKeyName, name),
|
||||
writeHealthDumpToFile,
|
||||
configuredOpenTelemetry,
|
||||
)
|
||||
)
|
||||
.valueOr(err => throw new RuntimeException(s"Failed to create domain bootstrap: $err"))
|
||||
|
||||
private def simClocks: Seq[SimClock] = {
|
||||
val clocks = clock +: (participants.running.map(_.clock) ++ domains.running.map(_.clock))
|
||||
val simclocks = clocks.collect { case sc: SimClock => sc }
|
||||
if (simclocks.sizeCompare(clocks) < 0)
|
||||
logger.warn(s"Found non-sim clocks, testing time service will be broken.")
|
||||
simclocks
|
||||
}
|
||||
|
||||
def addUserCloseable(closeable: AutoCloseable): Unit = userCloseables.append(closeable)
|
||||
|
||||
override def close(): Unit = blocking(this.synchronized {
|
||||
val closeActorSystem: AutoCloseable =
|
||||
Lifecycle.toCloseableActorSystem(actorSystem, logger, timeouts)
|
||||
|
||||
val closeExecutionContext: AutoCloseable =
|
||||
ExecutorServiceExtensions(executionContext)(logger, timeouts)
|
||||
val closeScheduler: AutoCloseable = ExecutorServiceExtensions(scheduler)(logger, timeouts)
|
||||
|
||||
val closeHealthServer: AutoCloseable = () => healthServer.foreach(_.close())
|
||||
|
||||
val closeHeadlessHealthAdministration: AutoCloseable =
|
||||
() => healthDumpGenerator.get.foreach(_.grpcAdminCommandRunner.close())
|
||||
|
||||
// the allNodes list is ordered in ideal startup order, so reverse to shutdown
|
||||
val instances =
|
||||
monitorO.toList ++ userCloseables ++ allNodes.reverse :+ metricsFactory :+ configuredOpenTelemetry :+ clock :+ closeHealthServer :+
|
||||
closeHeadlessHealthAdministration :+ executionSequencerFactory :+ closeActorSystem :+ closeExecutionContext :+
|
||||
closeScheduler
|
||||
logger.info("Closing environment...")
|
||||
Lifecycle.close((instances.toSeq): _*)(logger)
|
||||
})
|
||||
}
|
||||
|
||||
object Environment {
|
||||
|
||||
/** Ensure all java.util.logging statements are routed to slf4j instead and can be configured with logback.
|
||||
* This should be paired with adding a LevelChangePropagator to the logback configuration to avoid the performance impact
|
||||
* of translating all JUL log statements (regardless of whether they are being used).
|
||||
* See for more details: https://logback.qos.ch/manual/configuration.html#LevelChangePropagator
|
||||
*/
|
||||
def installJavaUtilLoggingBridge(): Unit = {
|
||||
if (!SLF4JBridgeHandler.isInstalled) {
|
||||
// we want everything going to slf4j so remove any default loggers
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger()
|
||||
SLF4JBridgeHandler.install()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait EnvironmentFactory[E <: Environment] {
|
||||
def create(
|
||||
config: E#Config,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
testingConfigInternal: TestingConfigInternal = TestingConfigInternal(),
|
||||
): E
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.environment
|
||||
|
||||
import com.digitalasset.canton.resource.DbMigrations
|
||||
|
||||
sealed trait StartupError extends Product with Serializable {
|
||||
|
||||
/** Node name */
|
||||
val name: String
|
||||
def message: String
|
||||
override def toString: String = s"${name}: $message"
|
||||
|
||||
}
|
||||
|
||||
/** The current action cannot be performed when the instance for the given name is running.
|
||||
*/
|
||||
final case class AlreadyRunning(name: String) extends StartupError {
|
||||
def message = s"node is already running: $name"
|
||||
}
|
||||
|
||||
final case class FailedDatabaseMigration(name: String, cause: DbMigrations.Error)
|
||||
extends StartupError {
|
||||
def message: String = s"failed to migrate database of $name: $cause"
|
||||
}
|
||||
|
||||
final case class FailedDatabaseVersionChecks(name: String, cause: DbMigrations.DatabaseVersionError)
|
||||
extends StartupError {
|
||||
def message: String = s"version checks failed for database of $name: $cause"
|
||||
}
|
||||
|
||||
final case class FailedDatabaseConfigChecks(name: String, cause: DbMigrations.DatabaseConfigError)
|
||||
extends StartupError {
|
||||
def message: String = s"config checks failed for database of $name: $cause"
|
||||
}
|
||||
|
||||
final case class FailedDatabaseRepairMigration(name: String, cause: DbMigrations.Error)
|
||||
extends StartupError {
|
||||
def message: String = s"failed to repair the database migration of $name: $cause"
|
||||
}
|
||||
|
||||
final case class DidntUseForceOnRepairMigration(name: String) extends StartupError {
|
||||
def message: String =
|
||||
s"repair_migration` is a command that may lead to data corruption in the worst case if an " +
|
||||
s"incompatible database migration is subsequently applied. To use it you need to call `$name.db.repair_migration(force=true)`. " +
|
||||
s"See `help($name.db.repair_migration)` for more details. "
|
||||
}
|
||||
|
||||
final case class StartFailed(name: String, message: String) extends StartupError
|
||||
|
||||
final case class ShutdownDuringStartup(name: String, message: String) extends StartupError
|
||||
|
||||
/** Trying to start the node when the database has pending migrations
|
||||
*/
|
||||
final case class PendingDatabaseMigration(name: String, pendingMigrationMessage: String)
|
||||
extends StartupError {
|
||||
def message = s"failed to initialize $name: $pendingMigrationMessage"
|
||||
}
|
||||
|
||||
sealed trait ShutdownError {
|
||||
val name: String
|
||||
def message: String
|
||||
|
||||
override def toString: String = s"${name}: $message"
|
||||
}
|
||||
|
||||
/** Configuration for the given name was not found in the CantonConfig
|
||||
*/
|
||||
final case class ConfigurationNotFound(name: String) extends StartupError with ShutdownError {
|
||||
def message = s"configuration not found: $name"
|
||||
}
|
@ -0,0 +1,440 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.environment
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.instances.future.*
|
||||
import cats.syntax.either.*
|
||||
import cats.syntax.foldable.*
|
||||
import cats.{Applicative, Id}
|
||||
import com.digitalasset.canton.DiscardOps
|
||||
import com.digitalasset.canton.concurrent.ExecutionContextIdlenessExecutorService
|
||||
import com.digitalasset.canton.config.{DbConfig, LocalNodeConfig, ProcessingTimeout, StorageConfig}
|
||||
import com.digitalasset.canton.domain.config.DomainConfig
|
||||
import com.digitalasset.canton.domain.{Domain, DomainNodeBootstrap, DomainNodeParameters}
|
||||
import com.digitalasset.canton.lifecycle.*
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.participant.*
|
||||
import com.digitalasset.canton.participant.config.LocalParticipantConfig
|
||||
import com.digitalasset.canton.participant.ledger.api.CantonLedgerApiServerWrapper
|
||||
import com.digitalasset.canton.participant.ledger.api.CantonLedgerApiServerWrapper.MigrateSchemaConfig
|
||||
import com.digitalasset.canton.resource.DbStorage.RetryConfig
|
||||
import com.digitalasset.canton.resource.{DbMigrations, DbMigrationsFactory}
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.concurrent.{ExecutionContext, Future, Promise, blocking}
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
/** Group of CantonNodes of the same type (domains, participants, sequencers). */
|
||||
trait Nodes[+Node <: CantonNode, +NodeBootstrap <: CantonNodeBootstrap[Node]]
|
||||
extends FlagCloseable {
|
||||
|
||||
type InstanceName = String
|
||||
|
||||
/** Returns the startup group (nodes in the same group will start together)
|
||||
*
|
||||
* Mediator & Topology manager automatically connect to a domain. Participants
|
||||
* require an external call to reconnectDomains. Therefore, we can start participants, sequencer and domain
|
||||
* nodes together, but we have to wait for the sequencers to be up before we can kick off mediators & topology managers.
|
||||
*/
|
||||
def startUpGroup: Int
|
||||
|
||||
/** Returns the names of all known nodes */
|
||||
def names(): Seq[InstanceName]
|
||||
|
||||
/** Start an individual node by name */
|
||||
def start(name: InstanceName)(implicit
|
||||
traceContext: TraceContext
|
||||
): EitherT[Future, StartupError, Unit]
|
||||
|
||||
def startAndWait(name: InstanceName)(implicit
|
||||
traceContext: TraceContext
|
||||
): Either[StartupError, Unit]
|
||||
|
||||
/** Is the named node running? */
|
||||
def isRunning(name: InstanceName): Boolean
|
||||
|
||||
/** Get the single running node */
|
||||
def getRunning(name: InstanceName): Option[NodeBootstrap]
|
||||
|
||||
/** Get the node while it is still being started. This is mostly useful during testing to access the node in earlier
|
||||
* stages of its initialization phase.
|
||||
*/
|
||||
def getStarting(name: InstanceName): Option[NodeBootstrap]
|
||||
|
||||
/** Stop the named node */
|
||||
def stop(name: InstanceName)(implicit
|
||||
traceContext: TraceContext
|
||||
): EitherT[Future, ShutdownError, Unit]
|
||||
|
||||
def stopAndWait(name: InstanceName)(implicit
|
||||
traceContext: TraceContext
|
||||
): Either[ShutdownError, Unit]
|
||||
|
||||
/** Get nodes that are currently running */
|
||||
def running: Seq[NodeBootstrap]
|
||||
|
||||
/** Independently run any pending database migrations for the named node */
|
||||
def migrateDatabase(name: InstanceName): Either[StartupError, Unit]
|
||||
|
||||
/** Independently repair the Flyway schema history table for the named node to reset Flyway migration checksums etc */
|
||||
def repairDatabaseMigration(name: InstanceName): Either[StartupError, Unit]
|
||||
}
|
||||
|
||||
private sealed trait ManagedNodeStage[T]
|
||||
|
||||
private final case class PreparingDatabase[T](
|
||||
promise: Promise[Either[StartupError, T]]
|
||||
) extends ManagedNodeStage[T]
|
||||
|
||||
private final case class StartingUp[T](
|
||||
promise: Promise[Either[StartupError, T]],
|
||||
node: T,
|
||||
) extends ManagedNodeStage[T]
|
||||
|
||||
private final case class Running[T](node: T) extends ManagedNodeStage[T]
|
||||
|
||||
/** Nodes group that can start nodes with the provided configuration and factory */
|
||||
class ManagedNodes[
|
||||
Node <: CantonNode,
|
||||
NodeConfig <: LocalNodeConfig,
|
||||
NodeParameters <: CantonNodeParameters,
|
||||
NodeBootstrap <: CantonNodeBootstrap[Node],
|
||||
](
|
||||
create: (String, NodeConfig) => NodeBootstrap,
|
||||
migrationsFactory: DbMigrationsFactory,
|
||||
override protected val timeouts: ProcessingTimeout,
|
||||
configs: Map[String, NodeConfig],
|
||||
parametersFor: String => CantonNodeParameters,
|
||||
override val startUpGroup: Int,
|
||||
protected val loggerFactory: NamedLoggerFactory,
|
||||
)(implicit ec: ExecutionContext)
|
||||
extends Nodes[Node, NodeBootstrap]
|
||||
with NamedLogging
|
||||
with HasCloseContext
|
||||
with FlagCloseableAsync {
|
||||
|
||||
private val nodes = TrieMap[InstanceName, ManagedNodeStage[NodeBootstrap]]()
|
||||
override lazy val names: Seq[InstanceName] = configs.keys.toSeq
|
||||
|
||||
override def running: Seq[NodeBootstrap] = nodes.values.toSeq.collect { case Running(node) =>
|
||||
node
|
||||
}
|
||||
|
||||
def startAndWait(name: InstanceName)(implicit
|
||||
traceContext: TraceContext
|
||||
): Either[StartupError, Unit] =
|
||||
timeouts.unbounded.await(s"Starting node $name")(start(name).value)
|
||||
|
||||
override def start(
|
||||
name: InstanceName
|
||||
)(implicit
|
||||
traceContext: TraceContext
|
||||
): EitherT[Future, StartupError, Unit] =
|
||||
EitherT
|
||||
.fromEither[Future](
|
||||
configs
|
||||
.get(name)
|
||||
.toRight(ConfigurationNotFound(name): StartupError)
|
||||
)
|
||||
.flatMap(startNode(name, _).map(_ => ()))
|
||||
|
||||
private def startNode(
|
||||
name: InstanceName,
|
||||
config: NodeConfig,
|
||||
): EitherT[Future, StartupError, NodeBootstrap] = if (isClosing)
|
||||
EitherT.leftT(ShutdownDuringStartup(name, "Won't start during shutdown"))
|
||||
else {
|
||||
def runStartup(
|
||||
promise: Promise[Either[StartupError, NodeBootstrap]]
|
||||
): EitherT[Future, StartupError, NodeBootstrap] = {
|
||||
val params = parametersFor(name)
|
||||
val startup = for {
|
||||
// start migration
|
||||
_ <- EitherT(Future { checkMigration(name, config.storage, params) })
|
||||
instance = {
|
||||
val instance = create(name, config)
|
||||
nodes.put(name, StartingUp(promise, instance)).discard
|
||||
instance
|
||||
}
|
||||
_ <-
|
||||
instance.start().leftMap { error =>
|
||||
instance.close() // clean up resources allocated during instance creation (e.g., db)
|
||||
StartFailed(name, error): StartupError
|
||||
}
|
||||
} yield {
|
||||
// register the running instance
|
||||
nodes.put(name, Running(instance)).discard
|
||||
instance
|
||||
}
|
||||
import com.digitalasset.canton.util.Thereafter.syntax.*
|
||||
promise.completeWith(startup.value)
|
||||
// remove node upon failure
|
||||
startup.thereafter {
|
||||
case Success(Right(_)) => ()
|
||||
case Success(Left(_)) =>
|
||||
nodes.remove(name).discard
|
||||
case Failure(_) =>
|
||||
nodes.remove(name).discard
|
||||
}
|
||||
}
|
||||
|
||||
blocking(synchronized {
|
||||
nodes.get(name) match {
|
||||
case Some(PreparingDatabase(promise)) => EitherT(promise.future)
|
||||
case Some(StartingUp(promise, _)) => EitherT(promise.future)
|
||||
case Some(Running(node)) => EitherT.rightT(node)
|
||||
case None =>
|
||||
val promise = Promise[Either[StartupError, NodeBootstrap]]()
|
||||
nodes
|
||||
.put(name, PreparingDatabase(promise))
|
||||
.discard // discard is okay as this is running in the sync block
|
||||
runStartup(promise) // startup will run async
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private def configAndParams(
|
||||
name: InstanceName
|
||||
): Either[StartupError, (NodeConfig, CantonNodeParameters)] = {
|
||||
for {
|
||||
config <- configs.get(name).toRight(ConfigurationNotFound(name): StartupError)
|
||||
_ <- checkNotRunning(name)
|
||||
params = parametersFor(name)
|
||||
} yield (config, params)
|
||||
}
|
||||
|
||||
override def migrateDatabase(name: InstanceName): Either[StartupError, Unit] = blocking(
|
||||
synchronized {
|
||||
for {
|
||||
cAndP <- configAndParams(name)
|
||||
(config, params) = cAndP
|
||||
_ <- runMigration(name, config.storage, params.devVersionSupport)
|
||||
} yield ()
|
||||
}
|
||||
)
|
||||
|
||||
override def repairDatabaseMigration(name: InstanceName): Either[StartupError, Unit] = blocking(
|
||||
synchronized {
|
||||
for {
|
||||
cAndP <- configAndParams(name)
|
||||
(config, params) = cAndP
|
||||
_ <- runRepairMigration(name, config.storage, params.devVersionSupport)
|
||||
} yield ()
|
||||
}
|
||||
)
|
||||
|
||||
override def isRunning(name: InstanceName): Boolean = nodes.contains(name)
|
||||
|
||||
override def getRunning(name: InstanceName): Option[NodeBootstrap] = nodes.get(name).collect {
|
||||
case Running(node) => node
|
||||
}
|
||||
|
||||
override def getStarting(name: InstanceName): Option[NodeBootstrap] = nodes.get(name).collect {
|
||||
case StartingUp(_, node) => node
|
||||
}
|
||||
|
||||
override def stop(
|
||||
name: InstanceName
|
||||
)(implicit
|
||||
traceContext: TraceContext
|
||||
): EitherT[Future, ShutdownError, Unit] =
|
||||
for {
|
||||
_ <- EitherT.fromEither[Future](
|
||||
configs.get(name).toRight[ShutdownError](ConfigurationNotFound(name))
|
||||
)
|
||||
_ <- nodes.get(name).traverse_(stopStage(name))
|
||||
} yield ()
|
||||
|
||||
override def stopAndWait(name: InstanceName)(implicit
|
||||
traceContext: TraceContext
|
||||
): Either[ShutdownError, Unit] =
|
||||
timeouts.unbounded.await(s"stopping node $name")(stop(name).value)
|
||||
|
||||
private def stopStage(name: InstanceName)(
|
||||
stage: ManagedNodeStage[NodeBootstrap]
|
||||
)(implicit
|
||||
traceContext: TraceContext,
|
||||
ec: ExecutionContext,
|
||||
): EitherT[Future, ShutdownError, Unit] = {
|
||||
EitherT(stage match {
|
||||
// wait for the node to complete startup
|
||||
case PreparingDatabase(promise) => promise.future
|
||||
case StartingUp(promise, _) => promise.future
|
||||
case Running(node) => Future.successful(Right(node))
|
||||
}).transform {
|
||||
case Left(_) =>
|
||||
// we can remap a startup failure to a success here, as we don't want the
|
||||
// startup failure to propagate into a shutdown failure
|
||||
Right(())
|
||||
case Right(node) =>
|
||||
nodes.remove(name).foreach {
|
||||
// if there were other processes messing with the node, we won't shutdown
|
||||
case Running(current) if node == current =>
|
||||
Lifecycle.close(node)(logger)
|
||||
case _ =>
|
||||
logger.info(s"Node $name has already disappeared.")
|
||||
}
|
||||
Right(())
|
||||
}
|
||||
}
|
||||
|
||||
override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = {
|
||||
val runningInstances = nodes.toList
|
||||
import TraceContext.Implicits.Empty.*
|
||||
runningInstances.map { case (name, stage) =>
|
||||
AsyncCloseable(s"node-$name", stopStage(name)(stage).value, timeouts.closing)
|
||||
}
|
||||
}
|
||||
|
||||
protected def runIfUsingDatabase[F[_]](storageConfig: StorageConfig)(
|
||||
fn: DbConfig => F[Either[StartupError, Unit]]
|
||||
)(implicit F: Applicative[F]): F[Either[StartupError, Unit]] = storageConfig match {
|
||||
case dbConfig: DbConfig => fn(dbConfig)
|
||||
case _ => F.pure(Right(()))
|
||||
}
|
||||
|
||||
// if database is fresh, we will migrate it. Otherwise, we will check if there is any pending migrations,
|
||||
// which need to be triggered manually.
|
||||
private def checkMigration(
|
||||
name: InstanceName,
|
||||
storageConfig: StorageConfig,
|
||||
params: CantonNodeParameters,
|
||||
): Either[StartupError, Unit] =
|
||||
runIfUsingDatabase[Id](storageConfig) { dbConfig =>
|
||||
val migrations = migrationsFactory.create(dbConfig, name, params.devVersionSupport)
|
||||
import TraceContext.Implicits.Empty.*
|
||||
logger.info(s"Setting up database schemas for $name")
|
||||
|
||||
def errorMapping(err: DbMigrations.Error): StartupError = {
|
||||
err match {
|
||||
case DbMigrations.PendingMigrationError(msg) => PendingDatabaseMigration(name, msg)
|
||||
case err: DbMigrations.FlywayError => FailedDatabaseMigration(name, err)
|
||||
case err: DbMigrations.DatabaseError => FailedDatabaseMigration(name, err)
|
||||
case err: DbMigrations.DatabaseVersionError => FailedDatabaseVersionChecks(name, err)
|
||||
case err: DbMigrations.DatabaseConfigError => FailedDatabaseConfigChecks(name, err)
|
||||
}
|
||||
}
|
||||
val retryConfig =
|
||||
if (storageConfig.parameters.failFastOnStartup) RetryConfig.failFast
|
||||
else RetryConfig.forever
|
||||
|
||||
val result = migrations
|
||||
.checkAndMigrate(params, retryConfig)
|
||||
.leftMap(errorMapping)
|
||||
|
||||
result.value.onShutdown(
|
||||
Left(ShutdownDuringStartup(name, "DB migration check interrupted due to shutdown"))
|
||||
)
|
||||
}
|
||||
|
||||
private def checkNotRunning(name: InstanceName): Either[StartupError, Unit] =
|
||||
if (isRunning(name)) Left(AlreadyRunning(name))
|
||||
else Right(())
|
||||
|
||||
private def runMigration(
|
||||
name: InstanceName,
|
||||
storageConfig: StorageConfig,
|
||||
devVersionSupport: Boolean,
|
||||
): Either[StartupError, Unit] =
|
||||
runIfUsingDatabase[Id](storageConfig) { dbConfig =>
|
||||
migrationsFactory
|
||||
.create(dbConfig, name, devVersionSupport)
|
||||
.migrateDatabase()
|
||||
.leftMap(FailedDatabaseMigration(name, _))
|
||||
.value
|
||||
.onShutdown(Left(ShutdownDuringStartup(name, "DB migration interrupted due to shutdown")))
|
||||
}
|
||||
|
||||
private def runRepairMigration(
|
||||
name: InstanceName,
|
||||
storageConfig: StorageConfig,
|
||||
devVersionSupport: Boolean,
|
||||
): Either[StartupError, Unit] =
|
||||
runIfUsingDatabase[Id](storageConfig) { dbConfig =>
|
||||
migrationsFactory
|
||||
.create(dbConfig, name, devVersionSupport)
|
||||
.repairFlywayMigration()
|
||||
.leftMap(FailedDatabaseRepairMigration(name, _))
|
||||
.value
|
||||
.onShutdown(
|
||||
Left(ShutdownDuringStartup(name, "DB repair migration interrupted due to shutdown"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class ParticipantNodes[B <: CantonNodeBootstrap[N], N <: CantonNode, PC <: LocalParticipantConfig](
|
||||
create: (String, PC) => B, // (nodeName, config) => bootstrap
|
||||
migrationsFactory: DbMigrationsFactory,
|
||||
timeouts: ProcessingTimeout,
|
||||
configs: Map[String, PC],
|
||||
parametersFor: String => ParticipantNodeParameters,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
)(implicit
|
||||
protected val executionContext: ExecutionContextIdlenessExecutorService
|
||||
) extends ManagedNodes[N, PC, ParticipantNodeParameters, B](
|
||||
create,
|
||||
migrationsFactory,
|
||||
timeouts,
|
||||
configs,
|
||||
parametersFor,
|
||||
startUpGroup = 0,
|
||||
loggerFactory,
|
||||
) {
|
||||
private def migrateIndexerDatabase(name: InstanceName): Either[StartupError, Unit] = {
|
||||
import TraceContext.Implicits.Empty.*
|
||||
|
||||
for {
|
||||
config <- configs.get(name).toRight(ConfigurationNotFound(name))
|
||||
parameters = parametersFor(name)
|
||||
_ = parameters.processingTimeouts.unbounded.await("migrate indexer database") {
|
||||
runIfUsingDatabase[Future](config.storage) { dbConfig =>
|
||||
CantonLedgerApiServerWrapper
|
||||
.migrateSchema(
|
||||
MigrateSchemaConfig(
|
||||
dbConfig,
|
||||
config.ledgerApi.additionalMigrationPaths,
|
||||
),
|
||||
loggerFactory,
|
||||
)
|
||||
.map(_.asRight)
|
||||
}
|
||||
}
|
||||
} yield ()
|
||||
}
|
||||
|
||||
override def migrateDatabase(name: InstanceName): Either[StartupError, Unit] =
|
||||
for {
|
||||
_ <- super.migrateDatabase(name)
|
||||
_ <- migrateIndexerDatabase(name)
|
||||
} yield ()
|
||||
}
|
||||
|
||||
object ParticipantNodes {
|
||||
type ParticipantNodesOld[PC <: LocalParticipantConfig] =
|
||||
ParticipantNodes[ParticipantNodeBootstrap, ParticipantNode, PC]
|
||||
type ParticipantNodesX[PC <: LocalParticipantConfig] =
|
||||
ParticipantNodes[ParticipantNodeBootstrapX, ParticipantNodeX, PC]
|
||||
}
|
||||
|
||||
class DomainNodes[DC <: DomainConfig](
|
||||
create: (String, DC) => DomainNodeBootstrap,
|
||||
migrationsFactory: DbMigrationsFactory,
|
||||
timeouts: ProcessingTimeout,
|
||||
configs: Map[String, DC],
|
||||
parameters: String => DomainNodeParameters,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
)(implicit
|
||||
protected val executionContext: ExecutionContextIdlenessExecutorService
|
||||
) extends ManagedNodes[Domain, DC, DomainNodeParameters, DomainNodeBootstrap](
|
||||
create,
|
||||
migrationsFactory,
|
||||
timeouts,
|
||||
configs,
|
||||
parameters,
|
||||
startUpGroup = 0,
|
||||
loggerFactory,
|
||||
)
|
@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.health
|
||||
|
||||
import com.digitalasset.canton.config.{CheckConfig, ProcessingTimeout}
|
||||
import com.digitalasset.canton.environment.Environment
|
||||
import com.digitalasset.canton.lifecycle.Lifecycle
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.metrics.HealthMetrics
|
||||
import com.digitalasset.canton.participant.ParticipantNode
|
||||
import com.digitalasset.canton.participant.admin.PingService
|
||||
import com.digitalasset.canton.time.{Clock, WallClock}
|
||||
import com.digitalasset.canton.topology.UniqueIdentifier
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.digitalasset.canton.util.EitherUtil
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
|
||||
import java.time.{Duration, Instant}
|
||||
import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference}
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
/** Check to determine a health check response */
|
||||
trait HealthCheck extends AutoCloseable {
|
||||
|
||||
/** Ask the check to decide whether we're healthy.
|
||||
* The future should complete successfully with a HealthCheckResult.
|
||||
* If the future fails this implies there was an error performing the check itself.
|
||||
*/
|
||||
def isHealthy(implicit traceContext: TraceContext): Future[HealthCheckResult]
|
||||
override def close(): Unit = ()
|
||||
}
|
||||
|
||||
/** Constant response for a health check (used by the always-healthy configuration) */
|
||||
final case class StaticHealthCheck(private val result: HealthCheckResult) extends HealthCheck {
|
||||
override def isHealthy(implicit traceContext: TraceContext): Future[HealthCheckResult] =
|
||||
Future.successful(result)
|
||||
}
|
||||
|
||||
/** Pings the supplied participant to determine health.
|
||||
* Will be considered unhealthy if unable to resolve the ping service for the participant alias (likely due to startup and initialization).
|
||||
* Ping success considered healthy, ping failure considered unhealthy.
|
||||
* If the ping future fails (rather than completing successfully with a failure), it will be converted to a unhealthy response and the exception will be logged at DEBUG level.
|
||||
*/
|
||||
class PingHealthCheck(
|
||||
environment: Environment,
|
||||
participantAlias: String,
|
||||
timeout: FiniteDuration,
|
||||
metrics: HealthMetrics,
|
||||
protected val loggerFactory: NamedLoggerFactory,
|
||||
)(implicit executionContext: ExecutionContext)
|
||||
extends HealthCheck
|
||||
with NamedLogging {
|
||||
|
||||
private val pingLatencyTimer = metrics.pingLatency
|
||||
|
||||
override def isHealthy(implicit traceContext: TraceContext): Future[HealthCheckResult] =
|
||||
getParticipant match {
|
||||
case Left(failed) => Future.successful(failed)
|
||||
case Right(participant) =>
|
||||
val partyId = participant.id.uid
|
||||
val pingService = participant.ledgerApiDependentCantonServices.adminWorkflowServices.ping
|
||||
ping(pingService, partyId)
|
||||
}
|
||||
|
||||
private def getParticipant: Either[HealthCheckResult, ParticipantNode] =
|
||||
for {
|
||||
init <- Option(
|
||||
environment.participants
|
||||
) // if this check is called before the collection has been initialized it will be null, so be very defensive
|
||||
.flatMap(_.getRunning(participantAlias))
|
||||
.toRight(Unhealthy("participant is not started"))
|
||||
participant <- init.getNode.toRight(Unhealthy("participant has not been initialized"))
|
||||
_ <- Either.cond(
|
||||
participant.readyDomains.exists(_._2),
|
||||
(),
|
||||
Unhealthy("participant is not connected to any domains"),
|
||||
)
|
||||
} yield participant
|
||||
|
||||
private def ping(pingService: PingService, partyId: UniqueIdentifier)(implicit
|
||||
traceContext: TraceContext
|
||||
): Future[HealthCheckResult] = {
|
||||
val timer = pingLatencyTimer.time()
|
||||
val started = Instant.now()
|
||||
val pingResult = for {
|
||||
result <- pingService.ping(Set(partyId.toProtoPrimitive), Set(), timeout.toMillis)
|
||||
} yield {
|
||||
timer.stop()
|
||||
result match {
|
||||
case PingService.Success(roundTripTime, _) =>
|
||||
logger.debug(s"Health check ping completed in ${roundTripTime}")
|
||||
Healthy
|
||||
case PingService.Failure =>
|
||||
val elapsedTime = Duration.between(started, Instant.now)
|
||||
logger.warn(s"Health check ping failed (elapsed time ${elapsedTime.toMillis}ms)")
|
||||
Unhealthy("ping failure")
|
||||
}
|
||||
}
|
||||
|
||||
pingResult recover { case NonFatal(ex) =>
|
||||
logger.warn("health check ping failed", ex)
|
||||
Unhealthy("ping failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** For components that simply flag whether they are active or not, just return that.
|
||||
* @param isActive should return a Right if the instance is active,
|
||||
* should return Left with a message to be returned on the health endpoint if not.
|
||||
*/
|
||||
class IsActiveCheck(
|
||||
isActive: () => Either[String, Unit],
|
||||
protected val loggerFactory: NamedLoggerFactory,
|
||||
) extends HealthCheck {
|
||||
override def isHealthy(implicit traceContext: TraceContext): Future[HealthCheckResult] =
|
||||
Future.successful {
|
||||
isActive().fold(Unhealthy, _ => Healthy)
|
||||
}
|
||||
}
|
||||
|
||||
/** Rather than executing a check for every isHealthy call periodically run the check and cache the result, and return this cached value for isHealthy.
|
||||
*/
|
||||
class PeriodicCheck(
|
||||
clock: Clock,
|
||||
interval: FiniteDuration,
|
||||
protected val loggerFactory: NamedLoggerFactory,
|
||||
)(check: HealthCheck)(implicit executionContext: ExecutionContext)
|
||||
extends HealthCheck
|
||||
with NamedLogging {
|
||||
|
||||
/** Once closed we should prevent checks from being run and scheduled */
|
||||
private val closed = new AtomicBoolean(false)
|
||||
|
||||
/** Cached value that will hold the previous health check result.
|
||||
* It is initialized by calling setupNextCheck so until the first result completes it will hold a pending future.
|
||||
*/
|
||||
private val lastCheck = new AtomicReference[Future[HealthCheckResult]](setupFirstCheck)
|
||||
|
||||
/** Returns the health check promise rather than updating lastCheck, so is suitable for initializing lastCheck */
|
||||
private def setupFirstCheck: Future[HealthCheckResult] = {
|
||||
val isHealthy = TraceContext.withNewTraceContext(check.isHealthy(_))
|
||||
isHealthy onComplete { _ =>
|
||||
setupNextCheck()
|
||||
}
|
||||
isHealthy
|
||||
}
|
||||
|
||||
/** Runs the check and when completed updates the value of lastCheck */
|
||||
private def runCheck(): Unit = if (!closed.get()) {
|
||||
val isHealthy = TraceContext.withNewTraceContext(check.isHealthy(_))
|
||||
|
||||
isHealthy onComplete { _ =>
|
||||
lastCheck.set(isHealthy)
|
||||
setupNextCheck()
|
||||
}
|
||||
}
|
||||
|
||||
private def setupNextCheck(): Unit =
|
||||
if (!closed.get()) {
|
||||
val _ = clock.scheduleAfter(_ => runCheck(), Duration.ofMillis(interval.toMillis))
|
||||
}
|
||||
|
||||
override def close(): Unit = {
|
||||
closed.set(true)
|
||||
Lifecycle.close(clock)(logger)
|
||||
}
|
||||
|
||||
override def isHealthy(implicit traceContext: TraceContext): Future[HealthCheckResult] =
|
||||
lastCheck.get()
|
||||
}
|
||||
|
||||
object HealthCheck {
|
||||
def apply(
|
||||
config: CheckConfig,
|
||||
metrics: HealthMetrics,
|
||||
timeouts: ProcessingTimeout,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
)(environment: Environment)(implicit system: ActorSystem): HealthCheck = {
|
||||
implicit val executionContext = system.dispatcher
|
||||
config match {
|
||||
case CheckConfig.AlwaysHealthy =>
|
||||
StaticHealthCheck(Healthy)
|
||||
case CheckConfig.Ping(participantAlias, interval, timeout) =>
|
||||
// only ping periodically rather than on every health check
|
||||
new PeriodicCheck(
|
||||
new WallClock(timeouts, loggerFactory.appendUnnamedKey("clock", "ping-health-check")),
|
||||
interval.underlying,
|
||||
loggerFactory,
|
||||
)(
|
||||
new PingHealthCheck(
|
||||
environment,
|
||||
participantAlias,
|
||||
timeout.underlying,
|
||||
metrics,
|
||||
loggerFactory,
|
||||
)
|
||||
)
|
||||
|
||||
case CheckConfig.IsActive(participantO) =>
|
||||
val configuredParticipants = environment.config.participantsByString
|
||||
|
||||
val participantName = participantO match {
|
||||
case Some(configName) =>
|
||||
if (configuredParticipants.contains(configName)) configName
|
||||
else sys.error(s"Participant with name '$configName' is not configured")
|
||||
case None =>
|
||||
configuredParticipants.headOption
|
||||
.map(_._1)
|
||||
.getOrElse(
|
||||
s"IsActive health check must be configured with the participant name to check as there are many participants configured for this environment"
|
||||
)
|
||||
}
|
||||
|
||||
def isActive: Either[String, Unit] =
|
||||
for {
|
||||
participant <- environment.participants
|
||||
.getRunning(participantName)
|
||||
.toRight("Participant is not running")
|
||||
runningParticipant <- participant.getNode.toRight("Participant is not initialized")
|
||||
_ <- EitherUtil.condUnitE(
|
||||
runningParticipant.sync.isActive(),
|
||||
"Participant is not active",
|
||||
)
|
||||
} yield ()
|
||||
|
||||
new IsActiveCheck(() => isActive, loggerFactory)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.health
|
||||
|
||||
/** Result of a health check */
|
||||
sealed trait HealthCheckResult
|
||||
|
||||
/** Everything that the check checks is healthy */
|
||||
object Healthy extends HealthCheckResult
|
||||
|
||||
/** The check deems something unhealthy
|
||||
* @param message User printable message describing why a unhealthy result was given
|
||||
*/
|
||||
final case class Unhealthy(message: String) extends HealthCheckResult
|
@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.health
|
||||
|
||||
import com.digitalasset.canton.config.RequireTypes.Port
|
||||
import com.digitalasset.canton.config.{HealthConfig, ProcessingTimeout}
|
||||
import com.digitalasset.canton.environment.Environment
|
||||
import com.digitalasset.canton.lifecycle.*
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.metrics.HealthMetrics
|
||||
import com.digitalasset.canton.tracing.TraceContext
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
import org.apache.pekko.http.scaladsl.Http
|
||||
import org.apache.pekko.http.scaladsl.marshalling.{Marshaller, ToResponseMarshaller}
|
||||
import org.apache.pekko.http.scaladsl.model.{HttpEntity, HttpResponse, StatusCodes}
|
||||
import org.apache.pekko.http.scaladsl.server.Directives.*
|
||||
import org.apache.pekko.http.scaladsl.server.Route
|
||||
import org.apache.pekko.http.scaladsl.server.directives.DebuggingDirectives
|
||||
|
||||
class HealthServer(
|
||||
check: HealthCheck,
|
||||
address: String,
|
||||
port: Port,
|
||||
protected override val timeouts: ProcessingTimeout,
|
||||
protected val loggerFactory: NamedLoggerFactory,
|
||||
)(implicit system: ActorSystem)
|
||||
extends FlagCloseableAsync
|
||||
with NamedLogging {
|
||||
|
||||
private val binding = {
|
||||
import TraceContext.Implicits.Empty.*
|
||||
timeouts.unbounded.await(s"Binding the health server")(
|
||||
Http().newServerAt(address, port.unwrap).bind(HealthServer.route(check))
|
||||
)
|
||||
}
|
||||
|
||||
override protected def closeAsync(): Seq[AsyncOrSyncCloseable] = {
|
||||
import TraceContext.Implicits.Empty.*
|
||||
List[AsyncOrSyncCloseable](
|
||||
AsyncCloseable("binding", binding.unbind(), timeouts.shutdownNetwork),
|
||||
SyncCloseable("check", Lifecycle.close(check)(logger)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object HealthServer {
|
||||
def apply(
|
||||
config: HealthConfig,
|
||||
metrics: HealthMetrics,
|
||||
timeouts: ProcessingTimeout,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
)(environment: Environment)(implicit system: ActorSystem): HealthServer = {
|
||||
val check = HealthCheck(config.check, metrics, timeouts, loggerFactory)(environment)
|
||||
|
||||
new HealthServer(check, config.server.address, config.server.port, timeouts, loggerFactory)
|
||||
}
|
||||
|
||||
/** Routes for powering the health server.
|
||||
* Provides:
|
||||
* GET /health => calls check and returns:
|
||||
* 200 if healthy
|
||||
* 500 if unhealthy
|
||||
* 500 if the check fails
|
||||
*/
|
||||
@VisibleForTesting
|
||||
private[health] def route(check: HealthCheck): Route = {
|
||||
implicit val _marshaller: ToResponseMarshaller[HealthCheckResult] =
|
||||
Marshaller.opaque {
|
||||
case Healthy =>
|
||||
HttpResponse(status = StatusCodes.OK, entity = HttpEntity("healthy"))
|
||||
case Unhealthy(message) =>
|
||||
HttpResponse(status = StatusCodes.InternalServerError, entity = HttpEntity(message))
|
||||
}
|
||||
|
||||
get {
|
||||
path("health") {
|
||||
DebuggingDirectives.logRequest("health-request") {
|
||||
DebuggingDirectives.logRequestResult("health-request-response") {
|
||||
complete(TraceContext.withNewTraceContext(check.isHealthy(_)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,382 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.metrics
|
||||
|
||||
import com.codahale.metrics
|
||||
import com.codahale.metrics.{Metric, MetricFilter, MetricRegistry}
|
||||
import com.daml.metrics.api.{MetricName, MetricsContext}
|
||||
import com.daml.metrics.grpc.DamlGrpcServerMetrics
|
||||
import com.daml.metrics.{
|
||||
ExecutorServiceMetrics,
|
||||
HealthMetrics as DMHealth,
|
||||
HistogramDefinition,
|
||||
JvmMetricSet,
|
||||
}
|
||||
import com.digitalasset.canton.DomainAlias
|
||||
import com.digitalasset.canton.buildinfo.BuildInfo
|
||||
import com.digitalasset.canton.config.DeprecatedConfigUtils.DeprecatedFieldsFor
|
||||
import com.digitalasset.canton.config.{DeprecatedConfigUtils, NonNegativeFiniteDuration}
|
||||
import com.digitalasset.canton.domain.metrics.{
|
||||
DomainMetrics,
|
||||
EnvMetrics,
|
||||
MediatorNodeMetrics,
|
||||
SequencerMetrics,
|
||||
}
|
||||
import com.digitalasset.canton.metrics.MetricHandle.{
|
||||
CantonDropwizardMetricsFactory,
|
||||
CantonOpenTelemetryMetricsFactory,
|
||||
}
|
||||
import com.digitalasset.canton.metrics.MetricsConfig.MetricsFilterConfig
|
||||
import com.digitalasset.canton.participant.metrics.ParticipantMetrics
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
import io.opentelemetry.api.OpenTelemetry
|
||||
import io.opentelemetry.api.metrics.Meter
|
||||
import io.prometheus.client.dropwizard.DropwizardExports
|
||||
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import scala.annotation.nowarn
|
||||
import scala.collection.concurrent.TrieMap
|
||||
|
||||
final case class MetricsConfig(
|
||||
reporters: Seq[MetricsReporterConfig] = Seq.empty,
|
||||
reportJvmMetrics: Boolean = false,
|
||||
reportExecutionContextMetrics: Boolean = false,
|
||||
histograms: Seq[HistogramDefinition] = Seq.empty,
|
||||
)
|
||||
|
||||
object MetricsReporterConfig {
|
||||
object DeprecatedImplicits {
|
||||
implicit def deprecatedDomainBaseConfig[X <: MetricsReporterConfig]: DeprecatedFieldsFor[X] =
|
||||
new DeprecatedFieldsFor[MetricsReporterConfig] {
|
||||
override def deprecatePath: List[DeprecatedConfigUtils.DeprecatedConfigPath[?]] = List(
|
||||
DeprecatedConfigUtils.DeprecatedConfigPath[String]("type", since = "2.6.0", Some("jmx")),
|
||||
DeprecatedConfigUtils.DeprecatedConfigPath[String]("type", since = "2.6.0", Some("csv")),
|
||||
DeprecatedConfigUtils
|
||||
.DeprecatedConfigPath[String]("type", since = "2.6.0", Some("graphite")),
|
||||
DeprecatedConfigUtils.DeprecatedConfigPath[String]("filters", since = "2.6.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait MetricsReporterConfig {
|
||||
def filters: Seq[MetricsFilterConfig]
|
||||
|
||||
def metricFilter: MetricFilter =
|
||||
(name: String, _: Metric) => filters.isEmpty || filters.exists(_.matches(name))
|
||||
}
|
||||
|
||||
sealed trait MetricsPrefix
|
||||
object MetricsPrefix {
|
||||
|
||||
/** Do not use a prefix */
|
||||
object NoPrefix extends MetricsPrefix
|
||||
|
||||
/** Use a static text string as prefix */
|
||||
final case class Static(prefix: String) extends MetricsPrefix
|
||||
|
||||
/** Uses the hostname as the prefix */
|
||||
object Hostname extends MetricsPrefix
|
||||
|
||||
def prefixFromConfig(prefix: MetricsPrefix): Option[String] = prefix match {
|
||||
case Hostname => Some(java.net.InetAddress.getLocalHost.getHostName)
|
||||
case NoPrefix => None
|
||||
case Static(prefix) => Some(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
object MetricsConfig {
|
||||
|
||||
final case class JMX(filters: Seq[MetricsFilterConfig] = Seq.empty) extends MetricsReporterConfig
|
||||
|
||||
final case class Csv(
|
||||
directory: File,
|
||||
interval: NonNegativeFiniteDuration = NonNegativeFiniteDuration.ofSeconds(5),
|
||||
filters: Seq[MetricsFilterConfig] = Seq.empty,
|
||||
) extends MetricsReporterConfig
|
||||
|
||||
final case class Graphite(
|
||||
address: String = "localhost",
|
||||
port: Int = 2003,
|
||||
prefix: MetricsPrefix = MetricsPrefix.Hostname,
|
||||
interval: NonNegativeFiniteDuration = NonNegativeFiniteDuration.ofSeconds(30),
|
||||
filters: Seq[MetricsFilterConfig] = Seq.empty,
|
||||
) extends MetricsReporterConfig
|
||||
|
||||
final case class Prometheus(address: String = "localhost", port: Int = 9100)
|
||||
extends MetricsReporterConfig {
|
||||
override def filters: Seq[MetricsFilterConfig] = Seq.empty
|
||||
}
|
||||
|
||||
final case class MetricsFilterConfig(
|
||||
startsWith: String = "",
|
||||
contains: String = "",
|
||||
endsWith: String = "",
|
||||
) {
|
||||
def matches(name: String): Boolean =
|
||||
name.startsWith(startsWith) && name.contains(contains) && name.endsWith(endsWith)
|
||||
}
|
||||
}
|
||||
|
||||
final case class MetricsFactory(
|
||||
reporters: Seq[metrics.Reporter],
|
||||
registry: metrics.MetricRegistry,
|
||||
reportJVMMetrics: Boolean,
|
||||
meter: Meter,
|
||||
factoryType: MetricsFactoryType,
|
||||
reportExecutionContextMetrics: Boolean,
|
||||
) extends AutoCloseable {
|
||||
|
||||
@deprecated("Use LabeledMetricsFactory", since = "2.7.0")
|
||||
val metricsFactory: MetricHandle.MetricsFactory =
|
||||
createUnlabeledMetricsFactory(MetricsContext.Empty, registry)
|
||||
|
||||
@nowarn("cat=deprecation") private val envMetrics = new EnvMetrics(metricsFactory)
|
||||
private val participants = TrieMap[String, ParticipantMetrics]()
|
||||
private val domains = TrieMap[String, DomainMetrics]()
|
||||
private val sequencers = TrieMap[String, SequencerMetrics]()
|
||||
private val mediators = TrieMap[String, MediatorNodeMetrics]()
|
||||
private val allNodeMetrics: Seq[TrieMap[String, ?]] =
|
||||
Seq(participants, domains, sequencers, mediators)
|
||||
private def nodeMetricsExcept(toExclude: TrieMap[String, ?]): Seq[TrieMap[String, ?]] =
|
||||
allNodeMetrics filterNot (_ eq toExclude)
|
||||
|
||||
val executionServiceMetrics: ExecutorServiceMetrics = new ExecutorServiceMetrics(
|
||||
createLabeledMetricsFactory(MetricsContext.Empty)
|
||||
)
|
||||
|
||||
object benchmark extends MetricsGroup(MetricName(MetricsFactory.prefix :+ "benchmark"), registry)
|
||||
|
||||
object health extends HealthMetrics(MetricName(MetricsFactory.prefix :+ "health"), registry)
|
||||
|
||||
// add default, system wide metrics to the metrics reporter
|
||||
if (reportJVMMetrics) {
|
||||
registry.registerAll(new JvmMetricSet) // register Daml repo JvmMetricSet
|
||||
JvmMetricSet.registerObservers() // requires OpenTelemetry to have the global lib setup
|
||||
}
|
||||
|
||||
private def newRegistry(prefix: String): metrics.MetricRegistry = {
|
||||
val nested = new metrics.MetricRegistry()
|
||||
registry.register(prefix, nested)
|
||||
nested
|
||||
}
|
||||
|
||||
def forParticipant(name: String): ParticipantMetrics = {
|
||||
participants.getOrElseUpdate(
|
||||
name, {
|
||||
val metricName = deduplicateName(name, "participant", participants)
|
||||
val participantMetricsContext =
|
||||
MetricsContext("participant" -> name, "component" -> "participant")
|
||||
val participantRegistry = newRegistry(metricName)
|
||||
new ParticipantMetrics(
|
||||
name,
|
||||
MetricsFactory.prefix,
|
||||
createUnlabeledMetricsFactory(participantMetricsContext, participantRegistry),
|
||||
createLabeledMetricsFactory(
|
||||
participantMetricsContext
|
||||
),
|
||||
participantRegistry,
|
||||
reportExecutionContextMetrics,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
def forEnv: EnvMetrics = envMetrics
|
||||
|
||||
def forDomain(name: String): DomainMetrics = {
|
||||
domains.getOrElseUpdate(
|
||||
name, {
|
||||
val metricName = deduplicateName(name, "domain", domains)
|
||||
val domainMetricsContext = MetricsContext("domain" -> name, "component" -> "domain")
|
||||
val labeledMetricsFactory =
|
||||
createLabeledMetricsFactory(domainMetricsContext)
|
||||
new DomainMetrics(
|
||||
MetricsFactory.prefix,
|
||||
createUnlabeledMetricsFactory(domainMetricsContext, newRegistry(metricName)),
|
||||
new DamlGrpcServerMetrics(labeledMetricsFactory, "domain"),
|
||||
new DMHealth(labeledMetricsFactory),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
def forSequencer(name: String): SequencerMetrics = {
|
||||
sequencers.getOrElseUpdate(
|
||||
name, {
|
||||
val metricName = deduplicateName(name, "sequencer", sequencers)
|
||||
val sequencerMetricsContext =
|
||||
MetricsContext("sequencer" -> name, "component" -> "sequencer")
|
||||
val labeledMetricsFactory = createLabeledMetricsFactory(
|
||||
sequencerMetricsContext
|
||||
)
|
||||
new SequencerMetrics(
|
||||
MetricsFactory.prefix,
|
||||
createUnlabeledMetricsFactory(sequencerMetricsContext, newRegistry(metricName)),
|
||||
new DamlGrpcServerMetrics(labeledMetricsFactory, "sequencer"),
|
||||
new DMHealth(labeledMetricsFactory),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
def forMediator(name: String): MediatorNodeMetrics = {
|
||||
mediators.getOrElseUpdate(
|
||||
name, {
|
||||
val metricName = deduplicateName(name, "mediator", mediators)
|
||||
val mediatorMetricsContext = MetricsContext("mediator" -> name, "component" -> "mediator")
|
||||
val labeledMetricsFactory =
|
||||
createLabeledMetricsFactory(mediatorMetricsContext)
|
||||
new MediatorNodeMetrics(
|
||||
MetricsFactory.prefix,
|
||||
createUnlabeledMetricsFactory(mediatorMetricsContext, newRegistry(metricName)),
|
||||
new DamlGrpcServerMetrics(labeledMetricsFactory, "mediator"),
|
||||
new DMHealth(labeledMetricsFactory),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** de-duplicate name if there is someone using the same name for another type of node (not sure that will ever happen)
|
||||
*/
|
||||
private def deduplicateName(
|
||||
name: String,
|
||||
nodeType: String,
|
||||
nodesToExclude: TrieMap[String, ?],
|
||||
): String =
|
||||
if (nodeMetricsExcept(nodesToExclude).exists(_.keySet.contains(name)))
|
||||
s"$nodeType-$name"
|
||||
else name
|
||||
|
||||
private def createLabeledMetricsFactory(extraContext: MetricsContext) = {
|
||||
factoryType match {
|
||||
case MetricsFactoryType.InMemory(provider) =>
|
||||
provider(extraContext)
|
||||
case MetricsFactoryType.External =>
|
||||
new CantonOpenTelemetryMetricsFactory(
|
||||
meter,
|
||||
globalMetricsContext = MetricsContext(
|
||||
"canton_version" -> BuildInfo.version
|
||||
).merge(extraContext),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def createUnlabeledMetricsFactory(
|
||||
extraContext: MetricsContext,
|
||||
registry: MetricRegistry,
|
||||
) = factoryType match {
|
||||
case MetricsFactoryType.InMemory(builder) => builder(extraContext)
|
||||
case MetricsFactoryType.External => new CantonDropwizardMetricsFactory(registry)
|
||||
}
|
||||
|
||||
/** returns the documented metrics by possibly creating fake participants / domains */
|
||||
def metricsDoc(): (Seq[MetricDoc.Item], Seq[MetricDoc.Item]) = {
|
||||
def sorted(lst: Seq[MetricDoc.Item]): Seq[MetricDoc.Item] =
|
||||
lst
|
||||
.groupBy(_.name)
|
||||
.flatMap(_._2.headOption.toList)
|
||||
.toSeq
|
||||
.sortBy(_.name)
|
||||
|
||||
val participantMetrics: ParticipantMetrics =
|
||||
participants.headOption.map(_._2).getOrElse(forParticipant("dummyParticipant"))
|
||||
val participantItems = MetricDoc.getItems(participantMetrics)
|
||||
val clientMetrics =
|
||||
MetricDoc.getItems(participantMetrics.domainMetrics(DomainAlias.tryCreate("<domain>")))
|
||||
val domainMetrics = MetricDoc.getItems(
|
||||
domains.headOption
|
||||
.map { case (_, domainMetrics) => domainMetrics }
|
||||
.getOrElse(forDomain("dummyDomain"))
|
||||
)
|
||||
|
||||
// the fake instances are fine here as we do this anyway only when we build and export the docs
|
||||
(sorted(participantItems ++ clientMetrics), sorted(domainMetrics))
|
||||
}
|
||||
|
||||
override def close(): Unit = reporters.foreach(_.close())
|
||||
|
||||
}
|
||||
|
||||
object MetricsFactory extends LazyLogging {
|
||||
|
||||
import MetricsConfig.*
|
||||
|
||||
val prefix: MetricName = MetricName("canton")
|
||||
|
||||
def forConfig(
|
||||
config: MetricsConfig,
|
||||
openTelemetry: OpenTelemetry,
|
||||
metricsFactoryType: MetricsFactoryType,
|
||||
): MetricsFactory = {
|
||||
val registry = new metrics.MetricRegistry()
|
||||
val reporter = registerReporter(config, registry)
|
||||
new MetricsFactory(
|
||||
reporter,
|
||||
registry,
|
||||
config.reportJvmMetrics,
|
||||
openTelemetry.meterBuilder("canton").build(),
|
||||
metricsFactoryType,
|
||||
config.reportExecutionContextMetrics,
|
||||
)
|
||||
}
|
||||
|
||||
private def registerReporter(
|
||||
config: MetricsConfig,
|
||||
registry: metrics.MetricRegistry,
|
||||
): Seq[metrics.Reporter] = {
|
||||
config.reporters.map {
|
||||
|
||||
case reporterConfig @ JMX(_filters) =>
|
||||
val reporter =
|
||||
metrics.jmx.JmxReporter.forRegistry(registry).filter(reporterConfig.metricFilter).build()
|
||||
logger.debug("Starting metrics reporting using JMX")
|
||||
reporter.start()
|
||||
reporter
|
||||
|
||||
case reporterConfig @ Csv(directory, interval, _filters) =>
|
||||
directory.mkdirs()
|
||||
logger.debug(s"Starting metrics reporting to csv-file ${directory.toString}")
|
||||
val reporter = metrics.CsvReporter
|
||||
.forRegistry(registry)
|
||||
.filter(reporterConfig.metricFilter)
|
||||
.formatFor(Locale.ENGLISH) // Format decimal numbers like "12345.12345".
|
||||
.build(directory)
|
||||
reporter.start(interval.unwrap.toMillis, TimeUnit.MILLISECONDS)
|
||||
reporter
|
||||
|
||||
case reporterConfig @ Graphite(address, port, prefix, interval, _filters) =>
|
||||
logger.debug(s"Starting metrics reporting for Graphite to $address:$port")
|
||||
val builder = metrics.graphite.GraphiteReporter
|
||||
.forRegistry(registry)
|
||||
.filter(reporterConfig.metricFilter)
|
||||
val reporter = MetricsPrefix
|
||||
.prefixFromConfig(prefix)
|
||||
.fold(builder)(str => builder.prefixedWith(str))
|
||||
.build(new metrics.graphite.Graphite(address, port))
|
||||
reporter.start(interval.unwrap.toMillis, TimeUnit.MILLISECONDS)
|
||||
reporter
|
||||
|
||||
// OpenTelemetry registers the prometheus collector during initialization
|
||||
case Prometheus(hostname, port) =>
|
||||
logger.debug(s"Exposing metrics for Prometheus on port $hostname:$port")
|
||||
new DropwizardExports(registry).register[DropwizardExports]()
|
||||
val reporter = new Reporters.Prometheus(hostname, port)
|
||||
reporter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HealthMetrics(prefix: MetricName, registry: metrics.MetricRegistry)
|
||||
extends MetricsGroup(prefix, registry) {
|
||||
|
||||
val pingLatency: metrics.Timer = timer("ping-latency")
|
||||
}
|
||||
|
||||
abstract class MetricsGroup(prefix: MetricName, registry: metrics.MetricRegistry) {
|
||||
|
||||
def timer(name: String): metrics.Timer = registry.timer(MetricName(prefix :+ name))
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.metrics
|
||||
|
||||
import com.codahale.metrics
|
||||
import io.opentelemetry.sdk.metrics.data.MetricData
|
||||
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
final case class MetricsSnapshot(
|
||||
timers: Map[String, metrics.Timer],
|
||||
counters: Map[String, metrics.Counter],
|
||||
gauges: Map[String, metrics.Gauge[_]],
|
||||
histograms: Map[String, metrics.Histogram],
|
||||
meters: Map[String, metrics.Meter],
|
||||
otelMetrics: Seq[MetricData],
|
||||
)
|
||||
|
||||
object MetricsSnapshot {
|
||||
|
||||
def apply(registry: metrics.MetricRegistry, reader: OnDemandMetricsReader): MetricsSnapshot = {
|
||||
MetricsSnapshot(
|
||||
timers = registry.getTimers.asScala.toMap,
|
||||
counters = registry.getCounters.asScala.toMap,
|
||||
gauges = registry.getGauges.asScala.toMap,
|
||||
histograms = registry.getHistograms.asScala.toMap,
|
||||
meters = registry.getMeters.asScala.toMap,
|
||||
otelMetrics = reader.read(),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.metrics
|
||||
|
||||
import com.codahale.metrics.Reporter
|
||||
import io.prometheus.client.exporter.HTTPServer
|
||||
|
||||
object Reporters {
|
||||
|
||||
class Prometheus(hostname: String, port: Int) extends Reporter {
|
||||
val server: HTTPServer = new HTTPServer.Builder().withHostname(hostname).withPort(port).build();
|
||||
override def close(): Unit = server.close()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
../../../../LICENSE-open-source-bundle.txt
|
15
canton-3x/community/app/src/main/resources/application.conf
Normal file
15
canton-3x/community/app/src/main/resources/application.conf
Normal file
@ -0,0 +1,15 @@
|
||||
pekko {
|
||||
loggers = ["org.apache.pekko.event.slf4j.Slf4jLogger"]
|
||||
loglevel = "INFO"
|
||||
|
||||
# the pekko-http server is only used for the health http server within canton.
|
||||
# It is difficult to configure HAProxy to supply a correct host header when
|
||||
# we've configured the http-check for the grpc server. So just default to
|
||||
# assuming requests with no host header are for localhost.
|
||||
# This should be revisited if we ever expose the server for anything beyond
|
||||
# health.
|
||||
http.server.default-host-header = "localhost"
|
||||
|
||||
# For canton applications we tear down pekko explicitly.
|
||||
jvm-shutdown-hooks = off
|
||||
}
|
281
canton-3x/community/app/src/main/resources/logback.xml
Normal file
281
canton-3x/community/app/src/main/resources/logback.xml
Normal file
@ -0,0 +1,281 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE configuration [
|
||||
<!ENTITY entityCorrelationIdTrailingSpaceReplace "%replace(tid:%mdc{trace-id} ){'tid: ', ''}- %msg%replace(, context: %marker){', context: $', ''}%replace( err-context:%mdc{err-context} ){' err-context: ', ''}%n">
|
||||
]>
|
||||
<configuration debug="false">
|
||||
<!-- propagate logback changes to jul handlers -->
|
||||
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
|
||||
<resetJUL>true</resetJUL>
|
||||
</contextListener>
|
||||
|
||||
<!-- whoever figures out how to define the encoder once and doesn't use copy pasta such as I did here wins a price! -->
|
||||
<if condition='isDefined("LOG_FORMAT_JSON")'>
|
||||
<then>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder class="com.digitalasset.canton.logging.CantonJsonEncoder"/>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>${LOG_LEVEL_STDOUT:-WARN}</level>
|
||||
</filter>
|
||||
</appender>
|
||||
</then>
|
||||
<else>
|
||||
<if condition='isDefined("INTERACTIVE_STDOUT")'>
|
||||
<then>
|
||||
<!-- show nice colors and omit date on log output if we are running interactively -->
|
||||
<!-- attempt to place the correlation-id with a trailing space, however replace with an empty string if it's empty -->
|
||||
<variable name="pattern" value="%highlight(%-5level %logger{10} &entityCorrelationIdTrailingSpaceReplace;)"/>
|
||||
<variable name="filter_class" value="com.digitalasset.canton.logging.ThrottleFilterEvaluator"/>
|
||||
</then>
|
||||
<else>
|
||||
<variable name="pattern" value="%date [%thread] %-5level %logger{35} &entityCorrelationIdTrailingSpaceReplace;"/>
|
||||
<!-- dummy filter which will have no effect (since its level is not set), to reduce code replication -->
|
||||
<variable name="filter_class" value="ch.qos.logback.classic.filter.ThresholdFilter"/>
|
||||
</else>
|
||||
</if>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${pattern}</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>${LOG_LEVEL_STDOUT:-WARN}</level>
|
||||
</filter>
|
||||
<filter class="${filter_class}" />
|
||||
</appender>
|
||||
</else>
|
||||
</if>
|
||||
|
||||
<if condition='isDefined("LOG_FILE_FLAT")'>
|
||||
<if condition='isDefined("LOG_LAST_ERRORS")'>
|
||||
<then><variable name="log_last_errors_filter" value="com.digitalasset.canton.logging.CantonFilterEvaluator"/></then>
|
||||
<!-- dummy filter which will have no effect, to reduce code replication -->
|
||||
<else><variable name="log_last_errors_filter" value="ch.qos.logback.classic.filter.ThresholdFilter"/></else>
|
||||
</if>
|
||||
<then>
|
||||
<if condition='isDefined("LOG_FORMAT_JSON")'>
|
||||
<then>
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${LOG_FILE_NAME:-log/canton.log}</file>
|
||||
<append>${LOG_FILE_APPEND:-true}</append>
|
||||
<!-- Allow for disabling flush on each log-line (faster, but may miss logs when crashing) -->
|
||||
<immediateFlush>${LOG_IMMEDIATE_FLUSH:-true}</immediateFlush>
|
||||
<encoder class="com.digitalasset.canton.logging.CantonJsonEncoder"/>
|
||||
<filter class="${log_last_errors_filter}" />
|
||||
</appender>
|
||||
</then>
|
||||
<else>
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${LOG_FILE_NAME:-log/canton.log}</file>
|
||||
<append>${LOG_FILE_APPEND:-true}</append>
|
||||
<!-- Allow for disabling flush on each log-line (faster, but may miss logs when crashing) -->
|
||||
<immediateFlush>${LOG_IMMEDIATE_FLUSH:-true}</immediateFlush>
|
||||
<encoder>
|
||||
<!-- attempt to place the correlation-id with a trailing space, however replace with an empty string if it's empty -->
|
||||
<pattern>%date [%thread] %-5level %logger{10} &entityCorrelationIdTrailingSpaceReplace;</pattern>
|
||||
</encoder>
|
||||
<filter class="${log_last_errors_filter}" />
|
||||
</appender>
|
||||
</else>
|
||||
</if>
|
||||
</then>
|
||||
<else>
|
||||
<if condition='isDefined("LOG_FILE_ROLLING")'>
|
||||
<then>
|
||||
<if condition='isDefined("LOG_FORMAT_JSON")'>
|
||||
<then>
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_FILE_NAME:-log/canton.log}</file>
|
||||
<append>true</append>
|
||||
<!-- Allow for disabling flush on each log-line (faster, but may miss logs when crashing) -->
|
||||
<immediateFlush>${LOG_IMMEDIATE_FLUSH:-true}</immediateFlush>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- hourly rollover and compress (gz), change pattern if you want different roll-overs -->
|
||||
<fileNamePattern>${LOG_FILE_NAME:-log/canton.log}.%d{${LOG_FILE_ROLLING_PATTERN:-yyyy-MM-dd}}.gz</fileNamePattern>
|
||||
<!-- keep max 12 archived log files -->
|
||||
<maxHistory>${LOG_FILE_HISTORY:-12}</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder class="com.digitalasset.canton.logging.CantonJsonEncoder"/>
|
||||
<filter class="${log_last_errors_filter}" />
|
||||
</appender>
|
||||
</then>
|
||||
<else>
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_FILE_NAME:-log/canton.log}</file>
|
||||
<append>true</append>
|
||||
<!-- Allow for disabling flush on each log-line (faster, but may miss logs when crashing) -->
|
||||
<immediateFlush>${LOG_IMMEDIATE_FLUSH:-true}</immediateFlush>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- hourly rollover and compress (gz), change pattern if you want different roll-overs -->
|
||||
<fileNamePattern>${LOG_FILE_NAME:-log/canton.log}.%d{${LOG_FILE_ROLLING_PATTERN:-yyyy-MM-dd}}.gz</fileNamePattern>
|
||||
<!-- keep max 12 archived log files -->
|
||||
<maxHistory>${LOG_FILE_HISTORY:-12}</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<!-- attempt to place the correlation-id with a trailing space, however replace with an empty string if it's empty -->
|
||||
<pattern>%date [%thread] %-5level %logger{35} &entityCorrelationIdTrailingSpaceReplace;</pattern>
|
||||
</encoder>
|
||||
<filter class="${log_last_errors_filter}" />
|
||||
</appender>
|
||||
</else>
|
||||
</if>
|
||||
</then>
|
||||
<else>
|
||||
<appender name="FILE" class="ch.qos.logback.core.helpers.NOPAppender"/>
|
||||
</else>
|
||||
</if>
|
||||
</else>
|
||||
</if>
|
||||
|
||||
<!-- By default, KMS audit logs will go to the Canton log file. To log them to a different file, set the KMS_LOG_FILE_NAME environment variable to the desired file path -->
|
||||
<if condition='isDefined("KMS_LOG_FILE_NAME")'>
|
||||
<then>
|
||||
<if condition='isDefined("LOG_FILE_FLAT")'>
|
||||
<then>
|
||||
<if condition='isDefined("LOG_FORMAT_JSON")'>
|
||||
<then>
|
||||
<appender name="KMS-FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${KMS_LOG_FILE_NAME:-log/canton_kms.log}</file>
|
||||
<append>${KMS_LOG_FILE_APPEND:-true}</append>
|
||||
<!-- Allow for disabling flush on each log-line (faster, but may miss logs when crashing) -->
|
||||
<immediateFlush>${KMS_LOG_IMMEDIATE_FLUSH:-true}</immediateFlush>
|
||||
<encoder class="com.digitalasset.canton.logging.CantonJsonEncoder"/>
|
||||
</appender>
|
||||
</then>
|
||||
<else>
|
||||
<appender name="KMS-FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${KMS_LOG_FILE_NAME:-log/canton_kms.log}</file>
|
||||
<append>${KMS_LOG_FILE_APPEND:-true}</append>
|
||||
<!-- Allow for disabling flush on each log-line (faster, but may miss logs when crashing) -->
|
||||
<immediateFlush>${KMS_LOG_IMMEDIATE_FLUSH:-true}</immediateFlush>
|
||||
<encoder>
|
||||
<!-- attempt to place the correlation-id with a trailing space, however replace with an empty string if it's empty -->
|
||||
<pattern>%date [%thread] %-5level %logger{10} &entityCorrelationIdTrailingSpaceReplace;</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
</else>
|
||||
</if>
|
||||
</then>
|
||||
<else>
|
||||
<if condition='isDefined("LOG_FILE_ROLLING")'>
|
||||
<then>
|
||||
<if condition='isDefined("LOG_FORMAT_JSON")'>
|
||||
<then>
|
||||
<appender name="KMS-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${KMS_LOG_FILE_NAME:-log/canton_kms.log}</file>
|
||||
<append>true</append>
|
||||
<!-- Allow for disabling flush on each log-line (faster, but may miss logs when crashing) -->
|
||||
<immediateFlush>${KMS_LOG_IMMEDIATE_FLUSH:-true}</immediateFlush>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- hourly rollover and compress (gz), change pattern if you want different roll-overs -->
|
||||
<fileNamePattern>${KMS_LOG_FILE_NAME:-log/canton_kms.log}.%d{${KMS_LOG_FILE_ROLLING_PATTERN:-yyyy-MM-dd}}.gz</fileNamePattern>
|
||||
<!-- keep all archived log files by default -->
|
||||
<maxHistory>${KMS_LOG_FILE_HISTORY:-0}</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder class="com.digitalasset.canton.logging.CantonJsonEncoder"/>
|
||||
</appender>
|
||||
</then>
|
||||
<else>
|
||||
<appender name="KMS-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${KMS_LOG_FILE_NAME:-log/canton_kms.log}</file>
|
||||
<append>true</append>
|
||||
<!-- Allow for disabling flush on each log-line (faster, but may miss logs when crashing) -->
|
||||
<immediateFlush>${KMS_LOG_IMMEDIATE_FLUSH:-true}</immediateFlush>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- hourly rollover and compress (gz), change pattern if you want different roll-overs -->
|
||||
<fileNamePattern>${KMS_LOG_FILE_NAME:-log/canton_kms.log}.%d{${KMS_LOG_FILE_ROLLING_PATTERN:-yyyy-MM-dd}}.gz</fileNamePattern>
|
||||
<!-- keep all archived log files by default -->
|
||||
<maxHistory>${KMS_LOG_FILE_HISTORY:-0}</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<!-- attempt to place the correlation-id with a trailing space, however replace with an empty string if it's empty -->
|
||||
<pattern>%date [%thread] %-5level %logger{35} &entityCorrelationIdTrailingSpaceReplace;</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
</else>
|
||||
</if>
|
||||
</then>
|
||||
</if>
|
||||
</else>
|
||||
</if>
|
||||
<logger name="com.digitalasset.canton.crypto.kms.aws.audit" level="${LOG_LEVEL_CANTON:-INFO}" additivity="false">
|
||||
<appender-ref ref="KMS-FILE"/>
|
||||
</logger>
|
||||
<logger name="com.digitalasset.canton.crypto.kms.gcp.audit" level="${LOG_LEVEL_CANTON:-INFO}" additivity="false">
|
||||
<appender-ref ref="KMS-FILE"/>
|
||||
</logger>
|
||||
</then>
|
||||
</if>
|
||||
|
||||
<!-- include the rewrite appender to rewrite certain log levels of certain messages -->
|
||||
<include resource="rewrite-appender.xml"/>
|
||||
<if condition='"false".equals(p("LOG_IMMEDIATE_FLUSH"))'>
|
||||
<then>
|
||||
<include resource="rewrite-async-appender.xml"/>
|
||||
</then>
|
||||
</if>
|
||||
|
||||
<!-- If log last errors is true, we set the DA loggers to debug log level but filter out at log level canton on the main log files -->
|
||||
<if condition='isDefined("LOG_LAST_ERRORS")'>
|
||||
<then>
|
||||
<logger name="com.digitalasset" level="DEBUG"/>
|
||||
<logger name="com.daml" level="DEBUG"/>
|
||||
</then>
|
||||
<else>
|
||||
<logger name="com.digitalasset" level="${LOG_LEVEL_CANTON:-INFO}"/>
|
||||
<logger name="com.daml" level="${LOG_LEVEL_CANTON:-INFO}"/>
|
||||
</else>
|
||||
</if>
|
||||
|
||||
<if condition='isDefined("LOG_LAST_ERRORS")'>
|
||||
<then>
|
||||
<appender name="FILE_LAST_ERRORS" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${LOG_LAST_ERRORS_FILE_NAME:-log/canton_errors.log}</file>
|
||||
<append>${LOG_FILE_APPEND:-true}</append>
|
||||
<encoder>
|
||||
<!-- attempt to place the correlation-id with a trailing space, however replace with an empty string if it's empty -->
|
||||
<pattern>%date [%thread] %-5level %logger{10} &entityCorrelationIdTrailingSpaceReplace;</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<if condition='"false".equals(p("LOG_IMMEDIATE_FLUSH"))'>
|
||||
<then>
|
||||
<variable name="REWRITE_LOG_LEVEL_MODE" value="REWRITE_LOG_LEVEL"/>
|
||||
</then>
|
||||
<else>
|
||||
<variable name="REWRITE_LOG_LEVEL_MODE" value="REWRITE_LOG_LEVEL_SYNC"/>
|
||||
</else>
|
||||
</if>
|
||||
|
||||
<!-- Buffer errors for the last_errors command before passing them on to the rewrite appender -->
|
||||
<appender name="LAST_ERRORS" class="com.digitalasset.canton.logging.LastErrorsAppender">
|
||||
<appender-ref ref="${REWRITE_LOG_LEVEL_MODE}" />
|
||||
<lastErrorsFileAppenderName>FILE_LAST_ERRORS</lastErrorsFileAppenderName>
|
||||
<appender-ref ref="FILE_LAST_ERRORS"/>
|
||||
</appender>
|
||||
</then>
|
||||
</if>
|
||||
|
||||
<root level="${LOG_LEVEL_ROOT:-INFO}"></root>
|
||||
<if condition='isDefined("LOG_LAST_ERRORS")'>
|
||||
<then>
|
||||
<root>
|
||||
<appender-ref ref="LAST_ERRORS" />
|
||||
</root>
|
||||
</then>
|
||||
<else>
|
||||
<if condition='"false".equals(p("LOG_IMMEDIATE_FLUSH"))'>
|
||||
<then>
|
||||
<root>
|
||||
<appender-ref ref="REWRITE_LOG_LEVEL" />
|
||||
</root>
|
||||
</then>
|
||||
<else>
|
||||
<root>
|
||||
<!-- default choice, chosen if LOG_IMMEDIATE_FLUSH is undefined -->
|
||||
<appender-ref ref="REWRITE_LOG_LEVEL_SYNC" />
|
||||
</root>
|
||||
</else>
|
||||
</if>
|
||||
</else>
|
||||
</if>
|
||||
|
||||
</configuration>
|
10
canton-3x/community/app/src/main/resources/repl/banner.txt
Normal file
10
canton-3x/community/app/src/main/resources/repl/banner.txt
Normal file
@ -0,0 +1,10 @@
|
||||
_____ _
|
||||
/ ____| | |
|
||||
| | __ _ _ __ | |_ ___ _ __
|
||||
| | / _` | '_ \| __/ _ \| '_ \
|
||||
| |___| (_| | | | | || (_) | | | |
|
||||
\_____\__,_|_| |_|\__\___/|_| |_|
|
||||
|
||||
Welcome to Canton!
|
||||
Type `help` to get started. `exit` to leave.
|
||||
|
@ -0,0 +1,219 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton
|
||||
|
||||
import better.files.File
|
||||
import cats.syntax.either.*
|
||||
import ch.qos.logback.classic.{Logger, LoggerContext}
|
||||
import ch.qos.logback.core.status.{ErrorStatus, Status, StatusListener, WarnStatus}
|
||||
import com.daml.nonempty.NonEmpty
|
||||
import com.digitalasset.canton.buildinfo.BuildInfo
|
||||
import com.digitalasset.canton.cli.{Cli, Command, LogFileAppender}
|
||||
import com.digitalasset.canton.config.ConfigErrors.CantonConfigError
|
||||
import com.digitalasset.canton.config.{CantonConfig, ConfigErrors, Generate}
|
||||
import com.digitalasset.canton.environment.{Environment, EnvironmentFactory}
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging}
|
||||
import com.digitalasset.canton.tracing.NoTracing
|
||||
import com.digitalasset.canton.version.ReleaseVersion
|
||||
import com.typesafe.config.{Config, ConfigFactory}
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
/** The Canton main application.
|
||||
*
|
||||
* Starts a set of domains and participant nodes.
|
||||
*/
|
||||
abstract class CantonAppDriver[E <: Environment] extends App with NamedLogging with NoTracing {
|
||||
|
||||
protected def environmentFactory: EnvironmentFactory[E]
|
||||
|
||||
protected def withManualStart(config: E#Config): E#Config
|
||||
|
||||
protected def additionalVersions: Map[String, String] = Map.empty
|
||||
|
||||
protected def printVersion(): Unit = {
|
||||
(Map(
|
||||
"Canton" -> BuildInfo.version,
|
||||
"Daml Libraries" -> BuildInfo.damlLibrariesVersion,
|
||||
"Supported Canton protocol versions" -> BuildInfo.protocolVersions.toString(),
|
||||
) ++ additionalVersions) foreach { case (name, version) =>
|
||||
Console.out.println(s"$name: $version")
|
||||
}
|
||||
}
|
||||
|
||||
// BE CAREFUL: Set the environment variables before you touch anything related to
|
||||
// logback as otherwise, the logback configuration will be read without these
|
||||
// properties being considered
|
||||
private val cliOptions = Cli.parse(args, printVersion()).getOrElse(sys.exit(1))
|
||||
cliOptions.installLogging()
|
||||
|
||||
// Fail, if the log configuration cannot be read.
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
private val loggerContext = LoggerFactory.getILoggerFactory.asInstanceOf[LoggerContext]
|
||||
private val logbackStatusManager = loggerContext.getStatusManager
|
||||
private val killingStatusListener: StatusListener = {
|
||||
case status @ (_: WarnStatus | _: ErrorStatus) =>
|
||||
Console.err.println(s"Unable to load log configuration.\n$status")
|
||||
Console.err.flush()
|
||||
sys.exit(-1)
|
||||
case _: Status => // ignore
|
||||
}
|
||||
logbackStatusManager.add(killingStatusListener)
|
||||
|
||||
// Use the root logger as named logger to avoid a prefix "CantonApp" in log files.
|
||||
override val loggerFactory: NamedLoggerFactory = NamedLoggerFactory.root
|
||||
|
||||
// Adjust root and canton loggers which works even if a custom logback.xml is defined
|
||||
Seq(
|
||||
(cliOptions.levelCanton, "com.digitalasset"),
|
||||
(cliOptions.levelCanton, "com.daml"),
|
||||
(cliOptions.levelRoot, org.slf4j.Logger.ROOT_LOGGER_NAME),
|
||||
)
|
||||
.foreach {
|
||||
case (Some(level), loggerName) =>
|
||||
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
|
||||
val root: Logger = LoggerFactory.getLogger(loggerName).asInstanceOf[Logger]
|
||||
root.setLevel(level)
|
||||
case (None, _) =>
|
||||
}
|
||||
|
||||
logger.info(s"Starting Canton version ${ReleaseVersion.current}")
|
||||
if (cliOptions.logTruncate) {
|
||||
cliOptions.logFileAppender match {
|
||||
case LogFileAppender.Rolling =>
|
||||
logger.warn(
|
||||
"Ignoring log file truncation request, as it only works with flat log files, but here we use rolling log files."
|
||||
)
|
||||
case LogFileAppender.Flat =>
|
||||
case LogFileAppender.Off =>
|
||||
}
|
||||
}
|
||||
|
||||
// Now that at least one line has been logged, deregister the killingStatusManager so that
|
||||
// Canton does not die on a warning status.
|
||||
logbackStatusManager.remove(killingStatusListener)
|
||||
|
||||
private val environmentRef: AtomicReference[Option[E]] = new AtomicReference(None)
|
||||
sys.runtime.addShutdownHook(new Thread(() => {
|
||||
try {
|
||||
logger.info("Shutting down...")
|
||||
environmentRef.get().foreach(_.close())
|
||||
logger.info("Shutdown complete.")
|
||||
} catch {
|
||||
case NonFatal(exception) =>
|
||||
logger.error("Failed to shut down successfully.", exception)
|
||||
} finally {
|
||||
LoggerFactory.getILoggerFactory match {
|
||||
case logbackLoggerContext: LoggerContext =>
|
||||
logger.info("Shutting down logger. Bye bye.")
|
||||
logbackLoggerContext.stop()
|
||||
case _ =>
|
||||
logger.warn(
|
||||
"Logback is not bound via slf4j. Cannot shut down logger, this could result in lost log-messages."
|
||||
)
|
||||
}
|
||||
}
|
||||
}))
|
||||
logger.debug("Registered shutdown-hook.")
|
||||
|
||||
val cantonConfig: E#Config = {
|
||||
val mergedUserConfigsE = NonEmpty.from(cliOptions.configFiles) match {
|
||||
case None if cliOptions.configMap.isEmpty =>
|
||||
Left(ConfigErrors.NoConfigFiles.Error())
|
||||
case None => Right(ConfigFactory.empty())
|
||||
case Some(neConfigFiles) => CantonConfig.parseAndMergeJustCLIConfigs(neConfigFiles)
|
||||
}
|
||||
val mergedUserConfigs =
|
||||
mergedUserConfigsE.valueOr { _ =>
|
||||
sys.exit(1)
|
||||
}
|
||||
|
||||
val configFromMap = {
|
||||
import scala.jdk.CollectionConverters.*
|
||||
ConfigFactory.parseMap(cliOptions.configMap.asJava)
|
||||
}
|
||||
val finalConfig = CantonConfig.mergeConfigs(mergedUserConfigs, Seq(configFromMap))
|
||||
|
||||
val loadedConfig = loadConfig(finalConfig) match {
|
||||
case Left(_) =>
|
||||
if (cliOptions.configFiles.sizeCompare(1) > 0)
|
||||
writeConfigToTmpFile(mergedUserConfigs)
|
||||
sys.exit(1)
|
||||
case Right(loaded) =>
|
||||
if (cliOptions.manualStart) withManualStart(loaded)
|
||||
else loaded
|
||||
}
|
||||
if (loadedConfig.monitoring.logging.logConfigOnStartup) {
|
||||
// we have two ways to log the config. both have their pro and cons.
|
||||
// full means we include default values. in such a case, it's hard to figure
|
||||
// out what really the config settings are.
|
||||
// the other method just uses the loaded `Config` object that doesn't have default
|
||||
// values, but therefore needs a separate way to handle the rendering
|
||||
logger.info(
|
||||
"Starting up with resolved config:\n" +
|
||||
(if (loadedConfig.monitoring.logging.logConfigWithDefaults)
|
||||
loadedConfig.dumpString
|
||||
else
|
||||
CantonConfig.renderForLoggingOnStartup(finalConfig))
|
||||
)
|
||||
}
|
||||
loadedConfig
|
||||
}
|
||||
|
||||
private def writeConfigToTmpFile(mergedUserConfigs: Config) = {
|
||||
val tmp = File.newTemporaryFile("canton-config-error-", ".conf")
|
||||
logger.error(
|
||||
s"An error occurred after parsing a config file that was obtained by merging multiple config " +
|
||||
s"files. The resulting merged-together config file, for which the error occurred, was written to '$tmp'."
|
||||
)
|
||||
tmp
|
||||
.write(
|
||||
mergedUserConfigs
|
||||
.root()
|
||||
.render(CantonConfig.defaultConfigRenderer)
|
||||
)
|
||||
.discard
|
||||
}
|
||||
|
||||
// verify that run script and bootstrap script aren't mixed
|
||||
if (cliOptions.bootstrapScriptPath.isDefined) {
|
||||
cliOptions.command match {
|
||||
case Some(Command.RunScript(_)) =>
|
||||
logger.error("--bootstrap script and run script are mutually exclusive")
|
||||
sys.exit(1)
|
||||
case Some(Command.Generate(_)) =>
|
||||
logger.error("--bootstrap script and generate are mutually exclusive")
|
||||
sys.exit(1)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
private lazy val bootstrapScript: Option[CantonScript] =
|
||||
cliOptions.bootstrapScriptPath
|
||||
.map(CantonScriptFromFile)
|
||||
|
||||
val runner: Runner[E] = cliOptions.command match {
|
||||
case Some(Command.Daemon) => new ServerRunner(bootstrapScript, loggerFactory)
|
||||
case Some(Command.RunScript(script)) => ConsoleScriptRunner(script, loggerFactory)
|
||||
case Some(Command.Generate(target)) =>
|
||||
Generate.process(target, cantonConfig)
|
||||
sys.exit(0)
|
||||
case _ =>
|
||||
new ConsoleInteractiveRunner(cliOptions.noTty, bootstrapScript, loggerFactory)
|
||||
}
|
||||
|
||||
val environment = environmentFactory.create(cantonConfig, loggerFactory)
|
||||
environmentRef.set(Some(environment)) // registering for graceful shutdown
|
||||
environment.startAndReconnect(cliOptions.autoConnectLocal) match {
|
||||
case Right(()) =>
|
||||
case Left(_) => sys.exit(1)
|
||||
}
|
||||
|
||||
runner.run(environment)
|
||||
|
||||
def loadConfig(config: Config): Either[CantonConfigError, E#Config]
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton
|
||||
|
||||
import com.digitalasset.canton.config.CantonCommunityConfig
|
||||
import com.digitalasset.canton.config.ConfigErrors.CantonConfigError
|
||||
import com.digitalasset.canton.environment.{
|
||||
CommunityEnvironment,
|
||||
CommunityEnvironmentFactory,
|
||||
EnvironmentFactory,
|
||||
}
|
||||
import com.typesafe.config.Config
|
||||
|
||||
object CantonCommunityApp extends CantonAppDriver[CommunityEnvironment] {
|
||||
|
||||
override def loadConfig(config: Config): Either[CantonConfigError, CantonCommunityConfig] =
|
||||
CantonCommunityConfig.load(config)
|
||||
|
||||
override protected def environmentFactory: EnvironmentFactory[CommunityEnvironment] =
|
||||
CommunityEnvironmentFactory
|
||||
|
||||
override protected def withManualStart(config: CantonCommunityConfig): CantonCommunityConfig =
|
||||
config.copy(parameters = config.parameters.copy(manualStart = true))
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton
|
||||
|
||||
import com.digitalasset.canton.console.{HeadlessConsole, InteractiveConsole}
|
||||
import com.digitalasset.canton.environment.Environment
|
||||
import com.digitalasset.canton.logging.{NamedLoggerFactory, NamedLogging, TracedLogger}
|
||||
import com.digitalasset.canton.tracing.{NoTracing, TraceContext}
|
||||
|
||||
import java.io.{File, OutputStream, StringWriter}
|
||||
import scala.io.Source
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
/** Result for exposing the process exit code.
|
||||
* All logging is expected to take place inside of the runner.
|
||||
*/
|
||||
trait Runner[E <: Environment] extends NamedLogging {
|
||||
|
||||
def run(environment: E): Unit
|
||||
}
|
||||
|
||||
class ServerRunner[E <: Environment](
|
||||
bootstrapScript: Option[CantonScript] = None,
|
||||
override val loggerFactory: NamedLoggerFactory,
|
||||
) extends Runner[E]
|
||||
with NoTracing {
|
||||
|
||||
def run(environment: E): Unit =
|
||||
try {
|
||||
def start(): Unit = {
|
||||
environment
|
||||
.startAll() match {
|
||||
case Right(_) => logger.info("Canton started")
|
||||
case Left(error) =>
|
||||
logger.error(s"Canton startup encountered problems: $error")
|
||||
// give up as we couldn't start everything successfully
|
||||
sys.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
def startWithBootstrap(script: CantonScript): Unit =
|
||||
ConsoleScriptRunner.run(environment, script, logger = logger) match {
|
||||
case Right(_unit) => logger.info("Bootstrap script successfully executed.")
|
||||
case Left(err) =>
|
||||
logger.error(s"Bootstrap script terminated with an error: $err")
|
||||
sys.exit(3)
|
||||
}
|
||||
|
||||
bootstrapScript.fold(start())(startWithBootstrap)
|
||||
} catch {
|
||||
case ex: Throwable =>
|
||||
logger.error(s"Unexpected error while running server: ${ex.getMessage}")
|
||||
logger.info("Exception causing error is:", ex)
|
||||
sys.exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
class ConsoleInteractiveRunner[E <: Environment](
|
||||
noTty: Boolean = false,
|
||||
bootstrapScript: Option[CantonScript],
|
||||
override val loggerFactory: NamedLoggerFactory,
|
||||
) extends Runner[E] {
|
||||
def run(environment: E): Unit = {
|
||||
val success =
|
||||
try {
|
||||
val consoleEnvironment = environment.createConsole()
|
||||
InteractiveConsole(consoleEnvironment, noTty, bootstrapScript, logger)
|
||||
} catch {
|
||||
case NonFatal(_) => false
|
||||
}
|
||||
sys.exit(if (success) 0 else 1)
|
||||
}
|
||||
}
|
||||
|
||||
class ConsoleScriptRunner[E <: Environment](
|
||||
scriptPath: CantonScript,
|
||||
override val loggerFactory: NamedLoggerFactory,
|
||||
) extends Runner[E] {
|
||||
private val Ok = 0
|
||||
private val Error = 1
|
||||
|
||||
override def run(environment: E): Unit = {
|
||||
val exitCode =
|
||||
ConsoleScriptRunner.run(environment, scriptPath, logger) match {
|
||||
case Right(_unit) =>
|
||||
Ok
|
||||
case Left(err) =>
|
||||
logger.error(s"Script execution failed: $err")(TraceContext.empty)
|
||||
Error
|
||||
}
|
||||
|
||||
sys.exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
private class CopyOutputWriter(parent: OutputStream, logger: TracedLogger)
|
||||
extends OutputStream
|
||||
with NoTracing {
|
||||
val buf = new StringWriter()
|
||||
override def write(b: Int): Unit = {
|
||||
if (b == '\n') {
|
||||
// strip the ansi color commands from the string
|
||||
val output = buf.toString.replaceAll("\u001B\\[[;\\d]*m", "")
|
||||
logger.info(s"Console stderr output: ${output}")
|
||||
buf.getBuffer.setLength(0)
|
||||
} else {
|
||||
buf.write(b)
|
||||
}
|
||||
parent.write(b)
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait CantonScript {
|
||||
def path: Option[File]
|
||||
def read(): Either[HeadlessConsole.IoError, String]
|
||||
}
|
||||
final case class CantonScriptFromFile(scriptPath: File) extends CantonScript {
|
||||
override val path = Some(scriptPath)
|
||||
override def read(): Either[HeadlessConsole.IoError, String] =
|
||||
readScript(scriptPath)
|
||||
|
||||
private def readScript(scriptPath: File): Either[HeadlessConsole.IoError, String] =
|
||||
for {
|
||||
path <- verifyScriptCanBeRead(scriptPath)
|
||||
content <- readScriptContent(path)
|
||||
} yield content
|
||||
|
||||
private def verifyScriptCanBeRead(scriptPath: File): Either[HeadlessConsole.IoError, File] =
|
||||
Either.cond(
|
||||
scriptPath.canRead,
|
||||
scriptPath,
|
||||
HeadlessConsole.IoError(s"Script file not readable: $scriptPath"),
|
||||
)
|
||||
|
||||
private def readScriptContent(scriptPath: File): Either[HeadlessConsole.IoError, String] = {
|
||||
val source = Source.fromFile(scriptPath)
|
||||
try {
|
||||
Right(source.mkString)
|
||||
} catch {
|
||||
case NonFatal(ex: Throwable) =>
|
||||
Left(HeadlessConsole.IoError(s"Failed to read script file: $ex"))
|
||||
} finally {
|
||||
source.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ConsoleScriptRunner extends NoTracing {
|
||||
def apply[E <: Environment](
|
||||
scriptPath: File,
|
||||
loggerFactory: NamedLoggerFactory,
|
||||
): ConsoleScriptRunner[E] =
|
||||
new ConsoleScriptRunner[E](CantonScriptFromFile(scriptPath), loggerFactory)
|
||||
def run[E <: Environment](
|
||||
environment: E,
|
||||
scriptPath: File,
|
||||
logger: TracedLogger,
|
||||
): Either[HeadlessConsole.HeadlessConsoleError, Unit] =
|
||||
run(environment, CantonScriptFromFile(scriptPath), logger)
|
||||
|
||||
def run[E <: Environment](
|
||||
environment: E,
|
||||
cantonScript: CantonScript,
|
||||
logger: TracedLogger,
|
||||
): Either[HeadlessConsole.HeadlessConsoleError, Unit] = {
|
||||
val consoleEnvironment = environment.createConsole()
|
||||
try {
|
||||
for {
|
||||
scriptCode <- cantonScript.read()
|
||||
_ <- HeadlessConsole.run(
|
||||
consoleEnvironment,
|
||||
scriptCode,
|
||||
cantonScript.path,
|
||||
// clone error stream such that we also log the error message
|
||||
// unfortunately, this means that if somebody outputs INFO to stdout,
|
||||
// he will observe the error twice
|
||||
transformer = x => x.copy(errorStream = new CopyOutputWriter(x.errorStream, logger)),
|
||||
logger = logger,
|
||||
)
|
||||
} yield ()
|
||||
} finally {
|
||||
consoleEnvironment.closeChannels()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,387 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.cli
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import com.digitalasset.canton.DiscardOps
|
||||
import com.digitalasset.canton.buildinfo.BuildInfo
|
||||
import scopt.OptionParser
|
||||
|
||||
import java.io.File
|
||||
import scala.annotation.nowarn
|
||||
|
||||
sealed trait LogFileAppender
|
||||
|
||||
object LogFileAppender {
|
||||
object Rolling extends LogFileAppender
|
||||
|
||||
object Flat extends LogFileAppender
|
||||
|
||||
object Off extends LogFileAppender
|
||||
}
|
||||
|
||||
sealed trait LogEncoder
|
||||
|
||||
object LogEncoder {
|
||||
object Plain extends LogEncoder
|
||||
|
||||
object Json extends LogEncoder
|
||||
}
|
||||
|
||||
/** CLI Options
|
||||
*
|
||||
* See the description for each argument in the CLI builder below.
|
||||
*/
|
||||
final case class Cli(
|
||||
configFiles: Seq[File] = Seq(),
|
||||
configMap: Map[String, String] = Map(),
|
||||
command: Option[Command] = None,
|
||||
noTty: Boolean = false,
|
||||
levelRoot: Option[Level] = None,
|
||||
levelCanton: Option[Level] = None,
|
||||
levelStdout: Level = Level.WARN,
|
||||
logFileAppender: LogFileAppender = LogFileAppender.Rolling,
|
||||
logFileRollingPattern: Option[String] = None,
|
||||
kmsLogFileRollingPattern: Option[String] = None,
|
||||
logFileHistory: Option[Int] = None,
|
||||
kmsLogFileHistory: Option[Int] = None,
|
||||
logTruncate: Boolean = false,
|
||||
logFileName: Option[String] = None,
|
||||
kmsLogFileName: Option[String] = None,
|
||||
logEncoder: LogEncoder = LogEncoder.Plain,
|
||||
logLastErrors: Boolean = true,
|
||||
logLastErrorsFileName: Option[String] = None,
|
||||
logImmediateFlush: Option[Boolean] = None,
|
||||
kmsLogImmediateFlush: Option[Boolean] = None,
|
||||
bootstrapScriptPath: Option[File] = None,
|
||||
manualStart: Boolean = false,
|
||||
autoConnectLocal: Boolean = false,
|
||||
) {
|
||||
|
||||
/** sets the properties our logback.xml is looking for */
|
||||
def installLogging(): Unit = {
|
||||
setLevel(levelRoot, "LOG_LEVEL_ROOT")
|
||||
|
||||
// The root level can override the canton level if root is configured with a lower level
|
||||
val overrideLevelCanton = (for {
|
||||
root <- levelRoot
|
||||
canton <- levelCanton
|
||||
} yield root.levelInt < canton.levelInt).getOrElse(false)
|
||||
if (overrideLevelCanton)
|
||||
setLevel(levelRoot, "LOG_LEVEL_CANTON")
|
||||
else
|
||||
setLevel(levelCanton, "LOG_LEVEL_CANTON")
|
||||
|
||||
setLevel(Some(levelStdout), "LOG_LEVEL_STDOUT")
|
||||
|
||||
if (command.isEmpty && !noTty) {
|
||||
// Inform logging that this is an interactive console running that needs some additional tweaks
|
||||
// for a good logging experience
|
||||
System.setProperty("INTERACTIVE_STDOUT", true.toString)
|
||||
}
|
||||
|
||||
System.setProperty("LOG_FILE_APPEND", (!logTruncate).toString)
|
||||
|
||||
Seq(
|
||||
"LOG_FILE_FLAT",
|
||||
"LOG_FILE_ROLLING",
|
||||
"LOG_FILE_NAME",
|
||||
"KMS_LOG_FILE_NAME",
|
||||
"LOG_FILE_ROLLING_PATTERN",
|
||||
"KMS_LOG_FILE_ROLLING_PATTERN",
|
||||
"LOG_FILE_HISTORY",
|
||||
"KMS_LOG_FILE_HISTORY",
|
||||
"LOG_LAST_ERRORS",
|
||||
"LOG_LAST_ERRORS_FILE_NAME",
|
||||
"LOG_FORMAT_JSON",
|
||||
"LOG_IMMEDIATE_FLUSH",
|
||||
"KMS_LOG_IMMEDIATE_FLUSH",
|
||||
).foreach(System.clearProperty(_).discard[String])
|
||||
logFileName.foreach(System.setProperty("LOG_FILE_NAME", _))
|
||||
kmsLogFileName.foreach(System.setProperty("KMS_LOG_FILE_NAME", _))
|
||||
logLastErrorsFileName.foreach(System.setProperty("LOG_LAST_ERRORS_FILE_NAME", _))
|
||||
logFileHistory.foreach(x => System.setProperty("LOG_FILE_HISTORY", x.toString))
|
||||
kmsLogFileHistory.foreach(x => System.setProperty("KMS_LOG_FILE_HISTORY", x.toString))
|
||||
logFileRollingPattern.foreach(System.setProperty("LOG_FILE_ROLLING_PATTERN", _))
|
||||
kmsLogFileRollingPattern.foreach(System.setProperty("KMS_LOG_FILE_ROLLING_PATTERN", _))
|
||||
logFileAppender match {
|
||||
case LogFileAppender.Rolling =>
|
||||
System.setProperty("LOG_FILE_ROLLING", "true").discard
|
||||
case LogFileAppender.Flat =>
|
||||
System.setProperty("LOG_FILE_FLAT", "true").discard
|
||||
case LogFileAppender.Off =>
|
||||
}
|
||||
if (logLastErrors)
|
||||
System.setProperty("LOG_LAST_ERRORS", "true").discard
|
||||
|
||||
logEncoder match {
|
||||
case LogEncoder.Plain =>
|
||||
case LogEncoder.Json =>
|
||||
System.setProperty("LOG_FORMAT_JSON", "true").discard
|
||||
}
|
||||
|
||||
logImmediateFlush.foreach(f => System.setProperty("LOG_IMMEDIATE_FLUSH", f.toString))
|
||||
kmsLogImmediateFlush.foreach(f => System.setProperty("KMS_LOG_IMMEDIATE_FLUSH", f.toString))
|
||||
}
|
||||
|
||||
private def setLevel(levelO: Option[Level], name: String): Unit = {
|
||||
val _ = levelO match {
|
||||
case Some(level) => System.setProperty(name, level.levelStr)
|
||||
case None => System.clearProperty(name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@nowarn(raw"msg=unused value of type .* \(add `: Unit` to discard silently\)")
|
||||
object Cli {
|
||||
// The `additionalVersions` parameter allows the enterprise CLI to output the version of additional,
|
||||
// enterprise-only dependencies (see `CantonAppDriver`).
|
||||
def parse(args: Array[String], printVersion: => Unit = ()): Option[Cli] =
|
||||
parser(printVersion).parse(args, Cli())
|
||||
|
||||
private def parser(printVersion: => Unit): OptionParser[Cli] =
|
||||
new scopt.OptionParser[Cli]("canton") {
|
||||
|
||||
private def inColumns(first: String = "", second: String = ""): String =
|
||||
f" $first%-25s$second"
|
||||
|
||||
head("Canton", s"v${BuildInfo.version}")
|
||||
|
||||
help('h', "help").text("Print usage")
|
||||
opt[Unit]("version")
|
||||
.text("Print versions")
|
||||
.action { (_, _) =>
|
||||
printVersion.discard
|
||||
sys.exit(0)
|
||||
}
|
||||
|
||||
opt[Seq[File]]('c', "config")
|
||||
.text(
|
||||
"Set configuration file(s).\n" +
|
||||
inColumns(second = "If several configuration files assign values to the same key,\n") +
|
||||
inColumns(second = "the last value is taken.")
|
||||
)
|
||||
.valueName("<file1>,<file2>,...")
|
||||
.unbounded()
|
||||
.action((files, cli) => cli.copy(configFiles = cli.configFiles ++ files))
|
||||
|
||||
opt[Map[String, String]]('C', "config key-value's")
|
||||
.text(
|
||||
"Set configuration key value pairs directly.\n" +
|
||||
inColumns(second = "Can be useful for providing simple short config info.")
|
||||
)
|
||||
.valueName("<key1>=<value1>,<key2>=<value2>")
|
||||
.unbounded()
|
||||
.action { (map, cli) =>
|
||||
cli.copy(configMap =
|
||||
map ++ cli.configMap
|
||||
) // the values on the right of the ++ operator are preferred for the same key. thus in case of repeated keys, the first defined is taken.
|
||||
}
|
||||
|
||||
opt[File]("bootstrap")
|
||||
.text("Set a script to run on startup")
|
||||
.valueName("<file>")
|
||||
.action((script, cli) => cli.copy(bootstrapScriptPath = Some(script)))
|
||||
|
||||
opt[Unit]("no-tty")
|
||||
.text("Do not use a tty")
|
||||
.action((_, cli) => cli.copy(noTty = true))
|
||||
|
||||
opt[Unit]("manual-start")
|
||||
.text("Don't automatically start the nodes")
|
||||
.action((_, cli) => cli.copy(manualStart = true))
|
||||
|
||||
opt[Unit]("auto-connect-local")
|
||||
.text("Automatically connect all local participants to all local domains")
|
||||
.action((_, cli) => cli.copy(autoConnectLocal = true))
|
||||
|
||||
note(inColumns(first = "-D<property>=<value>", second = "Set a JVM property value"))
|
||||
|
||||
note("\nLogging Options:") // Enforce a newline in the help text
|
||||
|
||||
opt[Unit]('v', "verbose")
|
||||
.text("Canton logger level -> DEBUG")
|
||||
.action((_, cli) => cli.copy(levelCanton = Some(Level.DEBUG)))
|
||||
|
||||
opt[Unit]("debug")
|
||||
.text("Console/stdout level -> INFO, root logger -> DEBUG")
|
||||
.action((_, cli) =>
|
||||
cli.copy(
|
||||
levelRoot = Some(Level.DEBUG),
|
||||
levelCanton = Some(Level.DEBUG),
|
||||
levelStdout = Level.INFO,
|
||||
)
|
||||
)
|
||||
|
||||
opt[Unit]("log-truncate")
|
||||
.text("Truncate log file on startup.")
|
||||
.action((_, cli) => cli.copy(logTruncate = true))
|
||||
|
||||
implicit val levelRead: scopt.Read[Level] = scopt.Read.reads(Level.valueOf)
|
||||
opt[Level]("log-level-root")
|
||||
.text("Log-level of the root logger")
|
||||
.valueName("<LEVEL>")
|
||||
.action((level, cli) => cli.copy(levelRoot = Some(level), levelCanton = Some(level)))
|
||||
|
||||
opt[Level]("log-level-canton")
|
||||
.text("Log-level of the Canton logger")
|
||||
.valueName("<LEVEL>")
|
||||
.action((level, cli) => cli.copy(levelCanton = Some(level)))
|
||||
|
||||
opt[Level]("log-level-stdout")
|
||||
.text("Log-level of stdout")
|
||||
.valueName("<LEVEL>")
|
||||
.action((level, cli) => cli.copy(levelStdout = level))
|
||||
|
||||
opt[String]("log-file-appender")
|
||||
.text("Type of log file appender")
|
||||
.valueName("rolling(default)|flat|off")
|
||||
.action((typ, cli) =>
|
||||
typ.toLowerCase match {
|
||||
case "rolling" => cli.copy(logFileAppender = LogFileAppender.Rolling)
|
||||
case "off" => cli.copy(logFileAppender = LogFileAppender.Off)
|
||||
case "flat" => cli.copy(logFileAppender = LogFileAppender.Flat)
|
||||
case _ =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Invalid command line argument: unknown log-file-appender $typ"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
opt[String]("log-file-name")
|
||||
.text("Name and location of log-file, default is log/canton.log")
|
||||
.action((name, cli) => cli.copy(logFileName = Some(name)))
|
||||
|
||||
opt[String]("kms-log-file-name")
|
||||
.text("Name and location of KMS log-file, default is log/canton_kms.log")
|
||||
.action((name, cli) => cli.copy(kmsLogFileName = Some(name)))
|
||||
|
||||
opt[Int]("log-file-rolling-history")
|
||||
.text("Number of history files to keep when using rolling log file appender.")
|
||||
.action((history, cli) => cli.copy(logFileHistory = Some(history)))
|
||||
|
||||
opt[Int]("kms-log-file-rolling-history")
|
||||
.text("Number of history KMS files to keep when using rolling log file appender.")
|
||||
.action((history, cli) => cli.copy(kmsLogFileHistory = Some(history)))
|
||||
|
||||
opt[String]("log-file-rolling-pattern")
|
||||
.text("Log file suffix pattern of rolling file appender. Default is 'yyyy-MM-dd'.")
|
||||
.action((pattern, cli) => cli.copy(logFileRollingPattern = Some(pattern)))
|
||||
|
||||
opt[String]("kms-log-file-rolling-pattern")
|
||||
.text("KMS log file suffix pattern of rolling file appender. Default is 'yyyy-MM-dd'.")
|
||||
.action((pattern, cli) => cli.copy(kmsLogFileRollingPattern = Some(pattern)))
|
||||
|
||||
opt[String]("log-encoder")
|
||||
.text("Log encoder: plain|json")
|
||||
.action {
|
||||
case ("json", cli) => cli.copy(logEncoder = LogEncoder.Json)
|
||||
case ("plain", cli) => cli.copy(logEncoder = LogEncoder.Plain)
|
||||
case (other, _) =>
|
||||
throw new IllegalArgumentException(s"Unsupported logging encoder $other")
|
||||
}
|
||||
|
||||
opt[Boolean]("log-immediate-flush")
|
||||
.text(
|
||||
"""Determines whether to immediately flush log output to the log file.
|
||||
|Enable to avoid an incomplete log file in case of a crash.
|
||||
|Disable to reduce the load on the disk caused by logging.""".stripMargin
|
||||
)
|
||||
.valueName("true(default)|false")
|
||||
.action((enabled, cli) => cli.copy(logImmediateFlush = Some(enabled)))
|
||||
|
||||
opt[Boolean]("kms-log-immediate-flush")
|
||||
.text(
|
||||
"""Determines whether to immediately flush KMS log output to the KMS log file.
|
||||
|Enable to avoid an incomplete log file in case of a crash.
|
||||
|Disable to reduce the load on the disk caused by logging.""".stripMargin
|
||||
)
|
||||
.valueName("true(default)|false")
|
||||
.action((enabled, cli) => cli.copy(kmsLogImmediateFlush = Some(enabled)))
|
||||
|
||||
opt[String]("log-profile")
|
||||
.text("Preconfigured logging profiles: (container)")
|
||||
.action((profile, cli) =>
|
||||
profile.toLowerCase match {
|
||||
case "container" =>
|
||||
cli.copy(
|
||||
logFileAppender = LogFileAppender.Rolling,
|
||||
logFileHistory = Some(10),
|
||||
logFileRollingPattern = Some("yyyy-MM-dd-HH"),
|
||||
levelStdout = Level.DEBUG,
|
||||
)
|
||||
case _ => throw new IllegalArgumentException(s"Unknown log profile $profile")
|
||||
}
|
||||
)
|
||||
|
||||
opt[Boolean]("log-last-errors")
|
||||
.text("Capture events for logging.last_errors command")
|
||||
.action((isEnabled, cli) => cli.copy(logLastErrors = isEnabled))
|
||||
|
||||
note("") // Enforce a newline in the help text
|
||||
|
||||
note("Use the JAVA_OPTS environment variable to set JVM parameters.")
|
||||
|
||||
note("") // Enforce a newline in the help text
|
||||
|
||||
cmd("daemon")
|
||||
.text(
|
||||
"Start all nodes automatically and run them without having a console (REPL).\n" +
|
||||
"Nodes can be controlled through the admin API."
|
||||
)
|
||||
.action((_, cli) => cli.copy(command = Some(Command.Daemon)))
|
||||
.children()
|
||||
|
||||
note("") // Enforce a newline in the help text
|
||||
|
||||
cmd("run")
|
||||
.text(
|
||||
"Run a console script.\n" +
|
||||
"Stop all nodes when the script has terminated."
|
||||
)
|
||||
.children(
|
||||
arg[File]("<file>")
|
||||
.text("the script to run")
|
||||
.action((script, cli) => cli.copy(command = Some(Command.RunScript(script))))
|
||||
)
|
||||
|
||||
note("") // Enforce a newline in the help text
|
||||
|
||||
implicit val readTarget: scopt.Read[Command.Generate.Target] = scopt.Read.reads {
|
||||
case "remote-config" => Command.Generate.RemoteConfig
|
||||
case x => throw new IllegalArgumentException(s"Unknown target $x")
|
||||
}
|
||||
cmd("generate")
|
||||
.text("Generate configurations")
|
||||
.children(
|
||||
arg[Command.Generate.Target]("<type>")
|
||||
.text("generation target (remote-config)")
|
||||
.action((target, cli) => cli.copy(command = Some(Command.Generate(target))))
|
||||
)
|
||||
|
||||
checkConfig(cli =>
|
||||
if (cli.configFiles.isEmpty && cli.configMap.isEmpty) {
|
||||
failure(
|
||||
"at least one config has to be defined either as files (-c) or as key-values (-C)"
|
||||
)
|
||||
} else success
|
||||
)
|
||||
|
||||
checkConfig(cli =>
|
||||
if (
|
||||
cli.autoConnectLocal && cli.command.exists {
|
||||
case Command.Daemon => false
|
||||
case Command.RunScript(_) => true
|
||||
case Command.Generate(_) => true
|
||||
}
|
||||
) {
|
||||
failure(s"auto-connect-local does not work with run-script or generate")
|
||||
} else success
|
||||
)
|
||||
|
||||
override def showUsageOnError: Option[Boolean] = Some(true)
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.cli
|
||||
|
||||
import java.io.File
|
||||
|
||||
sealed trait Command {}
|
||||
|
||||
object Command {
|
||||
|
||||
/** Run the process as a server (rather than an interactive repl)
|
||||
*/
|
||||
object Daemon extends Command
|
||||
|
||||
/** Run a console script then close
|
||||
*
|
||||
* @param scriptPath the path to the script
|
||||
*/
|
||||
final case class RunScript(scriptPath: File) extends Command
|
||||
|
||||
final case class Generate(target: Generate.Target) extends Command
|
||||
|
||||
object Generate {
|
||||
sealed trait Target
|
||||
|
||||
object RemoteConfig extends Target
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.config
|
||||
|
||||
import better.files.{File as BFile}
|
||||
import com.digitalasset.canton.cli.Command
|
||||
import com.digitalasset.canton.environment.Environment
|
||||
import pureconfig.ConfigWriter
|
||||
|
||||
object Generate {
|
||||
|
||||
private def write[A](name: String, prefix: String, config: A)(implicit
|
||||
configWriter: ConfigWriter[A]
|
||||
): Unit = {
|
||||
val _ = BFile(s"remote-${name}.conf")
|
||||
.write(
|
||||
s"canton.remote-${prefix}.${name} {" + System.lineSeparator() + configWriter
|
||||
.to(config)
|
||||
.render(CantonConfig.defaultConfigRenderer) + System.lineSeparator() + "}" + System
|
||||
.lineSeparator()
|
||||
)
|
||||
}
|
||||
|
||||
def process[E <: Environment](command: Command.Generate.Target, config: E#Config): Unit =
|
||||
command match {
|
||||
case Command.Generate.RemoteConfig =>
|
||||
val writers = new CantonConfig.ConfigWriters(confidential = false)
|
||||
import writers.*
|
||||
config.participantsByString.map(x => (x._1, x._2.toRemoteConfig)).foreach {
|
||||
case (name, config) =>
|
||||
write(name, "participants", config)
|
||||
}
|
||||
|
||||
config.domainsByString.map(x => (x._1, x._2.toRemoteConfig)).foreach {
|
||||
case (name, config) =>
|
||||
write(name, "domains", config)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import ammonite.interp.api.APIHolder
|
||||
import ammonite.util.Bind
|
||||
|
||||
/** ammonite requires a ApiHolder in this pattern to make items through bindings available within the dynamic Console environment.
|
||||
*/
|
||||
final case class BindingsHolder(bindings: IndexedSeq[Bind[_]])
|
||||
object BindingsBridge extends APIHolder[BindingsHolder]
|
@ -0,0 +1,259 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import ammonite.Main
|
||||
import ammonite.interp.Interpreter
|
||||
import ammonite.runtime.Frame
|
||||
import ammonite.util.Res.{Exception, Failing, Failure, Success}
|
||||
import ammonite.util.*
|
||||
import cats.syntax.either.*
|
||||
import com.digitalasset.canton.console.HeadlessConsole.{
|
||||
HeadlessConsoleError,
|
||||
convertAmmoniteResult,
|
||||
createInterpreter,
|
||||
initializePredef,
|
||||
runCode,
|
||||
}
|
||||
import com.digitalasset.canton.logging.TracedLogger
|
||||
import com.digitalasset.canton.tracing.NoTracing
|
||||
import com.digitalasset.canton.util.ErrorUtil
|
||||
import os.PathConvertible.*
|
||||
|
||||
import java.io.File
|
||||
import java.util.concurrent.atomic.{AtomicInteger, AtomicReference}
|
||||
import scala.util.Try
|
||||
|
||||
class HeadlessConsole(
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
transformer: Main => Main = identity,
|
||||
logger: TracedLogger,
|
||||
) extends AutoCloseable {
|
||||
|
||||
val (lock, baseOptions) =
|
||||
AmmoniteConsoleConfig.create(
|
||||
consoleEnvironment.environment.config.parameters.console,
|
||||
predefCode = "",
|
||||
welcomeBanner = None,
|
||||
isRepl = false,
|
||||
logger,
|
||||
)
|
||||
|
||||
private val interpreterO = new AtomicReference[Option[Interpreter]](None)
|
||||
private val currentLine = new AtomicInteger(10000000)
|
||||
|
||||
def init(): Either[HeadlessConsoleError, Unit] = {
|
||||
val options = transformer(baseOptions)
|
||||
for {
|
||||
interpreter <- Try(createInterpreter(options)).toEither.leftMap(
|
||||
HeadlessConsole.RuntimeError("Failed to initialize console", _)
|
||||
)
|
||||
bindings <- consoleEnvironment.bindings.leftMap(
|
||||
HeadlessConsole.RuntimeError("Unable to create the console bindings", _)
|
||||
)
|
||||
_ <- initializePredef(
|
||||
interpreter,
|
||||
bindings,
|
||||
consoleEnvironment.predefCode(_),
|
||||
logger,
|
||||
)
|
||||
} yield {
|
||||
interpreterO.set(Some(interpreter))
|
||||
}
|
||||
}
|
||||
|
||||
private def runModule(
|
||||
code: String,
|
||||
path: Option[File] = None,
|
||||
): Either[HeadlessConsoleError, Unit] =
|
||||
for {
|
||||
interpreter <- interpreterO
|
||||
.get()
|
||||
.toRight(HeadlessConsole.CompileError("Interpreter is not initialized"))
|
||||
_ <- runCode(interpreter, code, path, logger)
|
||||
} yield ()
|
||||
|
||||
def runLine(line: String): Either[HeadlessConsoleError, Unit] = for {
|
||||
interpreter <- interpreterO
|
||||
.get()
|
||||
.toRight(HeadlessConsole.CompileError("Interpreter is not initialized"))
|
||||
_ <- convertAmmoniteResult(
|
||||
interpreter
|
||||
.processExec(line, currentLine.incrementAndGet(), () => ()),
|
||||
logger,
|
||||
)
|
||||
} yield ()
|
||||
|
||||
override def close(): Unit = {
|
||||
lock.release()
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates an interpreter but with matching bindings to the InteractiveConsole for running scripts non-interactively
|
||||
*/
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
object HeadlessConsole extends NoTracing {
|
||||
|
||||
sealed trait HeadlessConsoleError
|
||||
|
||||
final case class CompileError(message: String) extends HeadlessConsoleError {
|
||||
override def toString: String = message
|
||||
}
|
||||
|
||||
final case class IoError(message: String) extends HeadlessConsoleError {
|
||||
override def toString: String = message
|
||||
}
|
||||
|
||||
final case class RuntimeError(message: String, cause: Throwable) extends HeadlessConsoleError {
|
||||
override def toString: String = {
|
||||
val messageWithSeparator = if (message.isEmpty) "" else message + " "
|
||||
val exceptionInfo = ErrorUtil.messageWithStacktrace(cause)
|
||||
messageWithSeparator + exceptionInfo
|
||||
}
|
||||
}
|
||||
|
||||
def run(
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
code: String,
|
||||
path: Option[File] = None,
|
||||
transformer: Main => Main = identity,
|
||||
logger: TracedLogger,
|
||||
): Either[HeadlessConsoleError, Unit] = {
|
||||
val console = new HeadlessConsole(consoleEnvironment, transformer, logger)
|
||||
try {
|
||||
for {
|
||||
_ <- console.init()
|
||||
_ <- console.runModule(code, path)
|
||||
} yield ()
|
||||
} finally {
|
||||
console.close()
|
||||
}
|
||||
}
|
||||
|
||||
private def initializePredef(
|
||||
interpreter: Interpreter,
|
||||
bindings: IndexedSeq[Bind[_]],
|
||||
interactivePredef: Boolean => String,
|
||||
logger: TracedLogger,
|
||||
): Either[HeadlessConsoleError, Unit] = {
|
||||
val bindingsPredef = generateBindPredef(bindings)
|
||||
|
||||
val holder = Seq(
|
||||
(
|
||||
// This has to match the object name of the implementation that extends APIHolder[_}
|
||||
objectClassNameWithoutSuffix(BindingsBridge.getClass),
|
||||
"canton",
|
||||
BindingsHolder(bindings),
|
||||
)
|
||||
)
|
||||
|
||||
val result = interpreter.initializePredef(
|
||||
basePredefs = Seq(
|
||||
PredefInfo(Name("BindingsPredef"), bindingsPredef, hardcoded = false, None),
|
||||
PredefInfo(
|
||||
Name("CantonImplicitPredef"),
|
||||
interactivePredef(false),
|
||||
hardcoded = false,
|
||||
None,
|
||||
),
|
||||
),
|
||||
customPredefs = Seq(),
|
||||
extraBridges = holder,
|
||||
)
|
||||
|
||||
// convert to an either and then map error if set to our own types
|
||||
result.toLeft(()).left.map(err => convertAmmoniteError(err._1, logger))
|
||||
}
|
||||
|
||||
private def runCode(
|
||||
interpreter: Interpreter,
|
||||
code: String,
|
||||
path: Option[File],
|
||||
logger: TracedLogger,
|
||||
): Either[HeadlessConsoleError, Unit] = {
|
||||
// the source details for our wrapper object that our code is compiled into
|
||||
val source = Util.CodeSource(
|
||||
wrapperName = Name("canton-script"),
|
||||
flexiblePkgName = Seq(Name("interpreter")),
|
||||
pkgRoot = Seq(Name("ammonite"), Name("canton")), // has to be rooted under ammonite
|
||||
path.map(path => os.Path(path.getAbsolutePath)),
|
||||
)
|
||||
|
||||
val result = interpreter.processModule(code, source, autoImport = false, "", hardcoded = false)
|
||||
|
||||
convertAmmoniteResult(result, logger)
|
||||
}
|
||||
|
||||
/** Converts a return value from Ammonite into:
|
||||
* - Unit if successful
|
||||
* - Our own error hierarchy if the Ammonite error could be mapped
|
||||
* @throws java.lang.RuntimeException If the value is unknown
|
||||
*/
|
||||
private def convertAmmoniteResult(
|
||||
result: Res[_],
|
||||
logger: TracedLogger,
|
||||
): Either[HeadlessConsoleError, Unit] =
|
||||
result match {
|
||||
case Success(_) =>
|
||||
Right(())
|
||||
case failing: Failing => Left(convertAmmoniteError(failing, logger))
|
||||
case unexpected =>
|
||||
logger.error("Unexpected result from ammonite: {}", unexpected)
|
||||
sys.error("Unexpected result from ammonite")
|
||||
}
|
||||
|
||||
/** Converts a failing return value from Ammonite into our own error types.
|
||||
* @throws java.lang.RuntimeException If the failing error is unknown
|
||||
*/
|
||||
private def convertAmmoniteError(result: Failing, logger: TracedLogger): HeadlessConsoleError =
|
||||
result match {
|
||||
case Failure(msg) => CompileError(msg)
|
||||
case Exception(cause, msg) => RuntimeError(msg, cause)
|
||||
case unexpected =>
|
||||
logger.error("Unexpected error result from ammonite: {}", unexpected)
|
||||
sys.error("Unexpected error result from ammonite")
|
||||
}
|
||||
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Null"))
|
||||
private def createInterpreter(options: Main): Interpreter = {
|
||||
val (colorsRef, printer) = Interpreter.initPrinters(
|
||||
options.colors,
|
||||
options.outputStream,
|
||||
options.errorStream,
|
||||
options.verboseOutput,
|
||||
)
|
||||
val frame = Frame.createInitial()
|
||||
|
||||
new Interpreter(
|
||||
compilerBuilder = ammonite.compiler.CompilerBuilder,
|
||||
parser = ammonite.compiler.Parsers,
|
||||
printer = printer,
|
||||
storage = options.storageBackend,
|
||||
wd = options.wd,
|
||||
colors = colorsRef,
|
||||
verboseOutput = options.verboseOutput,
|
||||
getFrame = () => frame,
|
||||
createFrame = () => sys.error("Session loading / saving is not supported"),
|
||||
initialClassLoader = null,
|
||||
replCodeWrapper = options.replCodeWrapper,
|
||||
scriptCodeWrapper = options.scriptCodeWrapper,
|
||||
alreadyLoadedDependencies = options.alreadyLoadedDependencies,
|
||||
)
|
||||
}
|
||||
|
||||
private def generateBindPredef(binds: IndexedSeq[Bind[_]]): String =
|
||||
binds.zipWithIndex
|
||||
.map { case (b, idx) =>
|
||||
s"""
|
||||
|val ${b.name} = com.digitalasset.canton.console
|
||||
| .BindingsBridge
|
||||
| .value0
|
||||
| .bindings($idx)
|
||||
| .value
|
||||
| .asInstanceOf[${b.typeTag.tpe}]
|
||||
""".stripMargin
|
||||
}
|
||||
.mkString(System.lineSeparator)
|
||||
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2023 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.canton.console
|
||||
|
||||
import ammonite.compiler.Parsers
|
||||
import ammonite.interp.Watchable
|
||||
import ammonite.util.{Res, *}
|
||||
import com.digitalasset.canton.CantonScript
|
||||
import com.digitalasset.canton.logging.TracedLogger
|
||||
import com.digitalasset.canton.tracing.NoTracing
|
||||
import com.digitalasset.canton.util.ResourceUtil.withResource
|
||||
|
||||
import java.io.{File, InputStream}
|
||||
import java.lang.System.lineSeparator
|
||||
import scala.io.Source
|
||||
import scala.util.Try
|
||||
|
||||
/** Will create a real REPL for interactive entry and evaluation of commands
|
||||
*/
|
||||
@SuppressWarnings(Array("org.wartremover.warts.Any"))
|
||||
object InteractiveConsole extends NoTracing {
|
||||
def apply(
|
||||
consoleEnvironment: ConsoleEnvironment,
|
||||
noTty: Boolean = false,
|
||||
bootstrapScript: Option[CantonScript] = None,
|
||||
logger: TracedLogger,
|
||||
): Boolean = {
|
||||
|
||||
val (_lock, baseOptions) = AmmoniteConsoleConfig.create(
|
||||
consoleEnvironment.environment.config.parameters.console,
|
||||
// for including implicit conversions
|
||||
predefCode =
|
||||
consoleEnvironment.predefCode(interactive = true, noTty = noTty) + lineSeparator(),
|
||||
welcomeBanner = Some(loadBanner()),
|
||||
isRepl = true,
|
||||
logger,
|
||||
)
|
||||
// where are never going to release the lock here
|
||||
|
||||
val options = baseOptions
|
||||
|
||||
// instead of using Main.run() from ammonite, we "inline"
|
||||
// that code here as "startup" in order to include the
|
||||
// bootstrap script in the beginning
|
||||
// the issue is that most bootstrap scripts require the bound repl arguments
|
||||
// (such as all, help, participant1, etc.), which are made available only here
|
||||
// so we can't run Main.runScript or so as the "result" of the script are lost then
|
||||
// in the REPL.
|
||||
def startup(replArgs: IndexedSeq[Bind[_]]): (Res[Any], Seq[(Watchable, Long)]) = {
|
||||
options.instantiateRepl(replArgs) match {
|
||||
case Left(missingPredefInfo) => missingPredefInfo
|
||||
case Right(repl) =>
|
||||
repl.initializePredef().getOrElse {
|
||||
// warm up the compilation
|
||||
val warmupThread = new Thread(() => {
|
||||
val _ = repl.warmup()
|
||||
})
|
||||
warmupThread.setDaemon(true)
|
||||
warmupThread.start()
|
||||
// load and run bootstrap script
|
||||
val initRes = bootstrapScript.map(fname => {
|
||||
// all we do is to write interp.load.module(...) into the console and let it interpret it
|
||||
// the lines here are stolen from Repl.warmup()
|
||||
logger.info(s"Running startup script $fname")
|
||||
val loadModuleCode = fname.path
|
||||
.map { (f: File) =>
|
||||
// Try to move the script to a temp file, otherwise the name of the file can shadow scala variables in the script
|
||||
Try {
|
||||
val tmp = better.files.File.newTemporaryFile()
|
||||
better.files.File(f.getAbsolutePath).copyTo(tmp, overwrite = true)
|
||||
logger.debug(
|
||||
s"Copied ${f.getAbsolutePath} to temporary file ${tmp.pathAsString}"
|
||||
)
|
||||
tmp.toJava
|
||||
}.fold(
|
||||
{ e =>
|
||||
logger.debug(
|
||||
s"Could not copy boostrap script to temp file, using original file",
|
||||
e,
|
||||
)
|
||||
f
|
||||
},
|
||||
identity,
|
||||
)
|
||||
}
|
||||
.map(p => "interp.load.module(os.Path(" + toStringLiteral(p.getAbsolutePath) + "))")
|
||||
.getOrElse(fname.read().getOrElse(""))
|
||||
val stmts = Parsers
|
||||
.split(loadModuleCode)
|
||||
.getOrElse(
|
||||
sys.error("Expected parser to always return a success or failure")
|
||||
) match { // `Parsers.split` returns an Option but should always be Some as we always provide code
|
||||
case Left(error) => sys.error(s"Unable to parse code: $error")
|
||||
case Right(parsed) => parsed
|
||||
}
|
||||
// if we run this with currentLine = 0, it will break the console output
|
||||
repl.interp.processLine(loadModuleCode, stmts, 10000000, silent = true, () => ())
|
||||
})
|
||||
|
||||
// now run the repl or exit if the bootstrap script failed
|
||||
initRes match {
|
||||
case Some(Res.Success(_)) | None =>
|
||||
val exitValue = Res.Success(repl.run())
|
||||
(exitValue.map(repl.beforeExit), repl.interp.watchedValues.toSeq)
|
||||
case Some(a @ Res.Exception(x, y)) =>
|
||||
val additionalMessage = if (y.isEmpty) "" else s", $y"
|
||||
logger.error(
|
||||
s"Running bootstrap script failed with an exception (${x.getMessage}$additionalMessage)!"
|
||||
)
|
||||
logger.debug("Ammonite exception thrown is", x)
|
||||
(a, repl.interp.watchedValues.toSeq)
|
||||
case Some(x) =>
|
||||
logger.error(s"Running bootstrap script failed with ${x}")
|
||||
(x, repl.interp.watchedValues.toSeq)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
consoleEnvironment.bindings match {
|
||||
case Left(exception) =>
|
||||
System.err.println(exception.getMessage)
|
||||
logger.debug("Unable to initialize the console bindings", exception)
|
||||
false
|
||||
case Right(bindings) =>
|
||||
val (result, _) = startup(bindings)
|
||||
result match {
|
||||
// as exceptions are caught when in the REPL this is almost certainly from code in the predef
|
||||
case Res.Exception(exception, _) =>
|
||||
System.err.println(exception.getMessage)
|
||||
logger.debug("Execution of interactive script returned exception", exception)
|
||||
false
|
||||
case Res.Failure(err) =>
|
||||
System.err.println(err)
|
||||
logger.debug(s"Execution of interactive script returned failure ${err}")
|
||||
false
|
||||
case _ =>
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Turns the given String into a string literal suitable for including in scala code.
|
||||
* Includes adding surrounding quotes.
|
||||
* e.g. `some\\path` will return `"some\\\\path"`
|
||||
*/
|
||||
private def toStringLiteral(raw: String): String = {
|
||||
// uses the scala reflection primitives but doesn't actually do any reflection
|
||||
import scala.reflect.runtime.universe.*
|
||||
|
||||
Literal(Constant(raw)).toString()
|
||||
}
|
||||
|
||||
private def loadBanner(): String = {
|
||||
val stream: InputStream = Option(getClass.getClassLoader.getResourceAsStream("repl/banner.txt"))
|
||||
.getOrElse(sys.error("banner resource not found"))
|
||||
|
||||
withResource(stream) { Source.fromInputStream(_).mkString }
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user