Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #26162: Install remove and change status of plugins from API #6120

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,15 @@ object JsonGlobalPluginLimits {
}
}

trait PluginSystemStatus {
sealed trait PluginSystemStatus {
def value: String
}
object PluginSystemStatus {
object PluginSystemStatus {
case object Enabled extends PluginSystemStatus { override val value: String = "enabled" }
case object Disabled extends PluginSystemStatus { override val value: String = "disabled" }

implicit val transformerJson: Transformer[PluginSystemStatus, JsonPluginSystemStatus] =
Transformer.derive[PluginSystemStatus, JsonPluginSystemStatus]
}

final case class JsonPluginDetails(
Expand All @@ -156,7 +159,7 @@ final case class JsonPluginDetails(
statusMessage: Option[String],
license: Option[PluginLicenseInfo]
)
object JsonPluginDetails {
object JsonPluginDetails {
implicit val encoderPluginSystemStatusRest: JsonEncoder[PluginSystemStatus] = JsonEncoder[String].contramap(_.value)
implicit val encoderPluginDetails: JsonEncoder[JsonPluginDetails] = DeriveJsonEncoder.gen
}
Expand Down Expand Up @@ -309,14 +312,20 @@ sealed trait PluginManagementError {
object PluginManagementError {
sealed trait Kind extends EnumEntry with Dotcase
object Kind extends Enum[Kind] {
case object LicenseNeededError extends Kind
case object LicenseExpiredError extends Kind
case object LicenseNearExpirationError extends Kind
case object AbiVersionError extends Kind
override def values: IndexedSeq[Kind] = findValues
}

case object LicenseNeededError extends PluginManagementError {
override def kind: Kind.LicenseNeededError.type = Kind.LicenseNeededError
override def displayMsg: String = "A license is needed for the plugin"
}

/**
* Sum type for license expiration error with disjoined cases
* Sum type for license expiration error with case disjunction
*/
sealed trait LicenseExpirationError extends PluginManagementError
case object LicenseExpiredError extends LicenseExpirationError {
Expand All @@ -339,6 +348,7 @@ object PluginManagementError {
)(implicit rudderFullVersion: String, abiVersion: Version): List[PluginManagementError] = {
List(
validateAbiVersion(rudderFullVersion, abiVersion),
validateLicenseNeeded(plugin.requiresLicense, plugin.license),
plugin.license.flatMap(l => validateLicenseExpiration(l.endDate))
).flatten
}
Expand All @@ -350,6 +360,16 @@ object PluginManagementError {
None
}

private def validateLicenseNeeded(
requiresLicense: Boolean,
license: Option[RudderPackagePlugin.LicenseInfo]
): Option[LicenseNeededError.type] = {
if (requiresLicense && license.isEmpty)
Some(LicenseNeededError)
else
None
}

/**
* license near expiration : 1 month before now.
*/
Expand All @@ -364,11 +384,17 @@ object PluginManagementError {

}

case class PluginId(value: String) extends AnyVal
object PluginId {
implicit val decoder: JsonDecoder[PluginId] = JsonDecoder[String].map(PluginId.apply)
implicit val transformer: Transformer[PluginId, String] = Transformer.derive[PluginId, String]
}

final case class JsonPluginSystemDetails(
id: String,
id: PluginId,
name: String,
description: String,
version: String,
version: Option[String],
status: JsonPluginSystemStatus,
statusMessage: Option[String],
abiVersion: Version,
Expand All @@ -392,6 +418,7 @@ object JsonPluginManagementError {
}

object JsonPluginSystemDetails {
implicit val encoderPluginId: JsonEncoder[PluginId] = JsonEncoder[String].contramap(_.value)
implicit val encoderPluginSystemStatusRest: JsonEncoder[JsonPluginSystemStatus] = JsonEncoder[String].contramap(_.entryName)
implicit val encoderPluginType: JsonEncoder[PluginType] = JsonEncoder[String].contramap(_.entryName)
implicit val encoderPluginManagementError: JsonEncoder[JsonPluginManagementError] =
Expand All @@ -402,14 +429,15 @@ object JsonPluginSystemDetails {

@jsonMemberNames(SnakeCase)
final case class RudderPackagePlugin(
name: String,
version: Option[String], // version is only available when installed
latestVersion: String,
installed: Boolean,
enabled: Boolean,
webappPlugin: Boolean,
description: String,
license: Option[RudderPackagePlugin.LicenseInfo]
name: String,
version: Option[String],
latestVersion: Option[String],
installed: Boolean,
enabled: Boolean,
webappPlugin: Boolean,
requiresLicense: Boolean,
description: String,
license: Option[RudderPackagePlugin.LicenseInfo]
)
object RudderPackagePlugin {
// types for passing implicits
Expand Down Expand Up @@ -461,7 +489,7 @@ object RudderPackagePlugin {
val _ = transformLicense // variable is used below
Transformer
.define[RudderPackagePlugin, JsonPluginSystemDetails]
.withFieldComputed(_.id, _.name)
.withFieldComputed(_.id, p => PluginId(p.name))
.withFieldComputed(
_.status,
p => {
Expand All @@ -472,7 +500,7 @@ object RudderPackagePlugin {
}
}
)
.withFieldComputed(_.version, l => l.version.getOrElse(l.latestVersion))
.withFieldComputed(_.version, l => l.version.orElse(l.latestVersion)) // version : only when installed
.withFieldConst(_.abiVersion, abiVersion) // field is computed upstream
.withFieldComputed(_.pluginType, p => if (p.webappPlugin) PluginType.Webapp else PluginType.Integration)
.withFieldConst(_.statusMessage, None)
Expand Down Expand Up @@ -524,11 +552,47 @@ class RudderPackageCmdService(configCmdLine: String) extends RudderPackageServic
}
}

override def installPlugins(plugins: Chunk[String]): IOResult[Unit] = ???
override def installPlugins(plugins: Chunk[String]): IOResult[Unit] = {
for {
configCmd <- configCmdRes.toIO
cmd = Cmd(configCmd._1, configCmd._2 ::: "install" :: plugins.toList, Map.empty, None)
packagesCmd <- RunNuCommand.run(cmd)
result <- packagesCmd.await
_ <- ZIO.when(result.code != 0) {
Inconsistency(
s"An error occurred while installing plugins with '${cmd.display}':\n code: ${result.code}\n stderr: ${result.stderr}\n stdout: ${result.stdout}"
).fail
}
} yield ()
}

override def removePlugins(plugins: Chunk[String]): IOResult[Unit] = ???
override def removePlugins(plugins: Chunk[String]): IOResult[Unit] = {
for {
configCmd <- configCmdRes.toIO
cmd = Cmd(configCmd._1, configCmd._2 ::: "remove" :: plugins.toList, Map.empty, None)
packagesCmd <- RunNuCommand.run(cmd)
result <- packagesCmd.await
_ <- ZIO.when(result.code != 0) {
Inconsistency(
s"An error occurred while removing plugins with '${cmd.display}':\n code: ${result.code}\n stderr: ${result.stderr}\n stdout: ${result.stdout}"
).fail
}
} yield ()
}

override def changePluginSystemStatus(status: PluginSystemStatus, plugins: Chunk[String]): IOResult[Unit] = ???
override def changePluginSystemStatus(status: PluginSystemStatus, plugins: Chunk[String]): IOResult[Unit] = {
for {
configCmd <- configCmdRes.toIO
cmd = Cmd(configCmd._1, configCmd._2 ::: status.value :: plugins.toList, Map.empty, None)
packagesCmd <- RunNuCommand.run(cmd)
result <- packagesCmd.await
_ <- ZIO.when(result.code != 0) {
Inconsistency(
s"An error occurred while changin plugin status to ${status.value} with '${cmd.display}':\n code: ${result.code}\n stderr: ${result.stderr}\n stdout: ${result.stdout}"
).fail
}
} yield ()
}

}

Expand All @@ -539,8 +603,41 @@ trait PluginSystemService {

def list(): IOResult[Chunk[JsonPluginSystemDetails]]

def install(plugins: Chunk[String]): IOResult[Unit]
def remove(plugins: Chunk[String]): IOResult[Unit]
def updateStatus(status: PluginSystemStatus, plugins: Chunk[String]): IOResult[Unit]
def install(plugins: Chunk[PluginId]): IOResult[Unit]
def remove(plugins: Chunk[PluginId]): IOResult[Unit]
def updateStatus(status: PluginSystemStatus, plugins: Chunk[PluginId]): IOResult[Unit]

}

/**
* Implementation for tests, will do any operation without any error
*/
class InMemoryPluginSystemService(ref: Ref[Map[PluginId, JsonPluginSystemDetails]]) extends PluginSystemService {
override def list(): UIO[Chunk[JsonPluginSystemDetails]] = {
ref.get.map(m => Chunk.fromIterable(m.values))
}

override def install(plugins: Chunk[PluginId]): UIO[Unit] = {
updatePluginStatus(JsonPluginSystemStatus.Enabled, plugins)
}

override def remove(plugins: Chunk[PluginId]): UIO[Unit] = {
updatePluginStatus(JsonPluginSystemStatus.Uninstalled, plugins)
}

override def updateStatus(status: PluginSystemStatus, plugins: Chunk[PluginId]): UIO[Unit] = {
updatePluginStatus(status.transformInto[JsonPluginSystemStatus], plugins)
}

private def updatePluginStatus(status: JsonPluginSystemStatus, plugins: Chunk[PluginId]) = {
ref.update(m => m ++ plugins.flatMap(id => m.get(id).map(p => id -> p.copy(status = status))))
}
}

object InMemoryPluginSystemService {
def make(initialPlugins: List[JsonPluginSystemDetails]): UIO[InMemoryPluginSystemService] = {
for {
ref <- Ref.make(initialPlugins.map(p => p.id -> p).toMap)
} yield new InMemoryPluginSystemService(ref)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,8 @@ trait StartsAtVersion16 extends EndpointSchema { val versions: ApiV.From = ApiV.
trait StartsAtVersion17 extends EndpointSchema { val versions: ApiV.From = ApiV.From(17) } // Rudder 7.3
trait StartsAtVersion18 extends EndpointSchema { val versions: ApiV.From = ApiV.From(18) } // Rudder 8.0
trait StartsAtVersion19 extends EndpointSchema { val versions: ApiV.From = ApiV.From(19) } // Rudder 8.1
trait StartsAtVersion20 extends EndpointSchema { val versions: ApiV.From = ApiV.From(20) }
trait StartsAtVersion20 extends EndpointSchema { val versions: ApiV.From = ApiV.From(20) } // Rudder 8.2
trait StartsAtVersion21 extends EndpointSchema { val versions: ApiV.From = ApiV.From(21) } // Rudder 8.3

// utility extension trait to define the kind of API
trait PublicApi extends EndpointSchema { val kind: ApiKind.Public.type = ApiKind.Public }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -649,18 +649,30 @@ sealed trait PluginInternalApi extends EnumEntry with EndpointSchema with Intern
}
object PluginInternalApi extends Enum[PluginInternalApi] with ApiModuleProvider[PluginInternalApi] {

case object ListPlugins extends PluginInternalApi with ZeroParam with StartsAtVersion20 with SortIndex {
case object ListPlugins extends PluginInternalApi with ZeroParam with StartsAtVersion21 with SortIndex {
val z: Int = implicitly[Line].value
val description = "List all plugins"
val (action, path) = GET / "pluginsinternal"
}
// case object UpdatePluginsSettings extends PluginApi with ZeroParam with StartsAtVersion14 with SortIndex {
// val z: Int = implicitly[Line].value
// override def dataContainer: Option[String] = None
// val description = "Update plugin system settings"
// val (action, path) = POST / "plugins" / "settings"
// }
case object InstallPlugins extends PluginInternalApi with ZeroParam with StartsAtVersion21 with SortIndex {
val z: Int = implicitly[Line].value
val description = "Install plugins"
val (action, path) = POST / "pluginsinternal" / "install"
override def dataContainer: Option[String] = None
}
case object RemovePlugins extends PluginInternalApi with ZeroParam with StartsAtVersion21 with SortIndex {
val z: Int = implicitly[Line].value
val description = "Remove plugins"
val (action, path) = POST / "pluginsinternal" / "remove"
override def dataContainer: Option[String] = None
}
def endpoints: List[PluginInternalApi] = values.toList.sortBy(_.z)
case object ChangePluginsStatus extends PluginInternalApi with OneParam with StartsAtVersion21 with SortIndex {
val z: Int = implicitly[Line].value
val description = "Change the status of plugins"
val (action, path) = POST / "pluginsinternal" / "{status}"
override def dataContainer: Option[String] = None
}

def values = findValues
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ object RudderJsonResponse {

def success[A](schema: ResponseSchema, id: Option[String], data: A): JsonRudderApiResponse[A] =
JsonRudderApiResponse(schema.action, id, "success", Some(data), None)

def success[A](schema: ResponseSchema, id: Option[String]): JsonRudderApiResponse[A] =
JsonRudderApiResponse(schema.action, id, "success", None, None)
}

//////////////////////////// Lift JSON response ////////////////////////////
Expand Down Expand Up @@ -170,6 +173,12 @@ object RudderJsonResponse {
generic.success(JsonRudderApiResponse.success(schema, None, Map(key -> objs)))
}
}
def successZero(schema: ResponseSchema)(implicit
prettify: Boolean
): LiftJsonResponse[JsonRudderApiResponse[String]] = {
implicit val enc: JsonEncoder[JsonRudderApiResponse[String]] = DeriveJsonEncoder.gen
generic.success(JsonRudderApiResponse.success(schema, None))
}
def successZero(schema: ResponseSchema, msg: String)(implicit
prettify: Boolean
): LiftJsonResponse[JsonRudderApiResponse[String]] = {
Expand Down Expand Up @@ -235,7 +244,7 @@ object RudderJsonResponse {
}
}

implicit class ToLiftResponseOne[A](result: IOResult[A]) {
implicit class ToLiftResponseOne[A](result: IOResult[A]) {
// ADT that matches error or success to determine the id value to use/compute
sealed trait IdTrace {
// if no computed id is given, we use the constant one
Expand Down Expand Up @@ -296,8 +305,27 @@ object RudderJsonResponse {
.runNow
}
}
// when you don't have any parameter, just a response
implicit class ToLiftResponseZero(result: IOResult[String]) {
// when you don't have any response, just a success
implicit class ToLiftResponseZero(result: IOResult[Unit]) {
def toLiftResponseZero(params: DefaultParams, schema: ResponseSchema): LiftResponse = {
implicit val prettify = params.prettify
result
.fold(
err => {
ApiLogger.ResponseError.info(err.fullMsg)
internalError(None, schema, err.fullMsg)
},
_ => successZero(schema)
)
.runNow
}
def toLiftResponseZero(params: DefaultParams, schema: EndpointSchema): LiftResponse = {
toLiftResponseZero(params, ResponseSchema.fromSchema(schema))
}
}

// when you don't have any parameter, just a message as response
implicit class ToLiftResponseZeroString(result: IOResult[String]) {
def toLiftResponseZero(params: DefaultParams, schema: ResponseSchema): LiftResponse = {
implicit val prettify = params.prettify
result
Expand Down
Loading