Skip to content

Commit

Permalink
Fixes #26235: License update needs a button and we need all license e…
Browse files Browse the repository at this point in the history
…nd dates
  • Loading branch information
clarktsiory committed Jan 21, 2025
1 parent 680ac56 commit ffb8420
Show file tree
Hide file tree
Showing 16 changed files with 877 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ object JsonGlobalPluginLimits {
res.copy(licensees = sortedDistinctLicensees)
}

private def comp[A](a: Option[A], b: Option[A], compare: (A, A) => A): Option[A] = (a, b) match {
def comp[A](a: Option[A], b: Option[A], compare: (A, A) => A): Option[A] = (a, b) match {
case (None, None) => None
case (Some(x), None) => Some(x)
case (None, Some(y)) => Some(y)
Expand Down Expand Up @@ -413,7 +413,7 @@ object PluginManagementError {
}

case class PluginId(value: String) extends AnyVal
object PluginId {
object PluginId {
implicit val decoder: JsonDecoder[PluginId] = JsonDecoder[String].mapOrFail(parse)
implicit val encoder: JsonEncoder[PluginId] = JsonEncoder[String].contramap(_.value)
implicit val transformer: Transformer[PluginId, String] = Transformer.derive[PluginId, String]
Expand All @@ -431,8 +431,72 @@ object PluginId {
}
}

/**
* Global license information about many plugins, aggregated such that :
* - more than 1 distinct licensees can exist for the collection of all plugins
* - end date of plugins licenses are aggregated, so that we know how many plugin license expire at a given date
**/
final case class JsonPluginsLicense(
licensees: Option[NonEmptyChunk[String]],
startDate: Option[ZonedDateTime],
endDates: Option[Map[ZonedDateTime, Int]],
maxNodes: Option[Int]
) {
import JsonGlobalPluginLimits.*
import JsonPluginsLicense.*
def combine(that: JsonPluginsLicense): JsonPluginsLicense = {
// for efficiency : check equality and hash first before field comparison,
// as it will mostly be the case because license information should be similar
if (this == that) this
else {
JsonPluginsLicense(
comp[NonEmptyChunk[String]](this.licensees, that.licensees, _ ++ _),
comp[ZonedDateTime](this.startDate, that.startDate, (x, y) => if (x.isAfter(y)) x else y),
comp[Map[ZonedDateTime, Int]](this.endDates, that.endDates, (x, y) => x.addValues(y)),
comp[Int](this.maxNodes, that.maxNodes, (x, y) => if (x < y) x else y)
)
}
}
}
object JsonPluginsLicense {
import DateFormaterService.JodaTimeToJava
implicit val dateFieldEncoder: JsonFieldEncoder[ZonedDateTime] =
JsonFieldEncoder[String].contramap(DateFormaterService.serializeZDT)
implicit val encoder: JsonEncoder[JsonPluginsLicense] = DeriveJsonEncoder.gen[JsonPluginsLicense]

def empty: JsonPluginsLicense = JsonPluginsLicense(None, None, None, None)

def fromLicenseInfo(info: PluginLicenseInfo): JsonPluginsLicense = {
JsonPluginsLicense(
Some(NonEmptyChunk(info.licensee)),
Some(info.startDate.toJava),
Some(Map((info.endDate.toJava, 1))),
info.maxNodes
)
}

private def from(licenses: NonEmptyChunk[PluginLicenseInfo]): JsonPluginsLicense = {
val res = licenses.reduceMapLeft(fromLicenseInfo) { case (lim, lic) => lim.combine(fromLicenseInfo(lic)) }
val sortedDistinctLicensees = res.licensees.map(_.sorted.distinct).flatMap(NonEmptyChunk.fromChunk)
res.copy(licensees = sortedDistinctLicensees)
}

def from(licenses: Chunk[PluginLicenseInfo]): Option[JsonPluginsLicense] = {
NonEmptyChunk
.fromChunk(licenses)
.map(from(_))
.flatMap(r => Option.when(r != empty)(r))
}

implicit class EndDatesOps(x: Map[ZonedDateTime, Int]) {
def addValues(y: Map[ZonedDateTime, Int]): Map[ZonedDateTime, Int] = {
(x.toList ::: y.toList).groupBy { case (date, _) => date }.map { case (k, v) => (k, v.map { case (_, n) => n }.sum) }.toMap
}
}
}

final case class JsonPluginsSystemDetails(
license: Option[JsonGlobalPluginLimits],
license: Option[JsonPluginsLicense],
plugins: Chunk[JsonPluginSystemDetails]
)
object JsonPluginsSystemDetails {
Expand All @@ -441,7 +505,7 @@ object JsonPluginsSystemDetails {
implicit val encoder: JsonEncoder[JsonPluginsSystemDetails] = DeriveJsonEncoder.gen[JsonPluginsSystemDetails]

def buildDetails(plugins: Chunk[JsonPluginSystemDetails]): JsonPluginsSystemDetails = {
val limits = JsonGlobalPluginLimits.getGlobalLimits(plugins.flatMap(_.license))
val limits = JsonPluginsLicense.from(plugins.flatMap(_.license))
JsonPluginsSystemDetails(limits, plugins)
}
}
Expand Down Expand Up @@ -577,7 +641,7 @@ object RudderPackagePlugin {
trait RudderPackageService {
import RudderPackageService.*

def updateBase(): IOResult[Option[CredentialError]]
def update(): IOResult[Option[CredentialError]]

def listAllPlugins(): IOResult[Chunk[RudderPackagePlugin]]

Expand Down Expand Up @@ -617,7 +681,7 @@ class RudderPackageCmdService(configCmdLine: String) extends RudderPackageServic
case h :: tail => Right((h, tail))
}

override def updateBase(): IOResult[Option[CredentialError]] = {
override def update(): IOResult[Option[CredentialError]] = {
// In case of error we need to check the result
for {
res <- runCmd("update" :: Nil)
Expand Down Expand Up @@ -693,8 +757,8 @@ class RudderPackageCmdService(configCmdLine: String) extends RudderPackageServic
*/
trait PluginSystemService {

def list(): IOResult[Either[CredentialError, Chunk[JsonPluginSystemDetails]]]

def updateIndex(): IOResult[Option[CredentialError]]
def list(): IOResult[Chunk[JsonPluginSystemDetails]]
def install(plugins: Chunk[PluginId]): IOResult[Unit]
def remove(plugins: Chunk[PluginId]): IOResult[Unit]
def updateStatus(status: PluginSystemStatus, plugins: Chunk[PluginId]): IOResult[Unit]
Expand All @@ -705,8 +769,12 @@ trait PluginSystemService {
* Implementation for tests, will do any operation without any error
*/
class InMemoryPluginSystemService(ref: Ref[Map[PluginId, JsonPluginSystemDetails]]) extends PluginSystemService {
override def list(): UIO[Either[CredentialError, Chunk[JsonPluginSystemDetails]]] = {
ref.get.map(m => Right(Chunk.fromIterable(m.values)))
override def updateIndex(): IOResult[Option[CredentialError]] = {
ZIO.none
}

override def list(): UIO[Chunk[JsonPluginSystemDetails]] = {
ref.get.map(m => Chunk.fromIterable(m.values))
}

override def install(plugins: Chunk[PluginId]): UIO[Unit] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,12 @@ sealed trait PluginInternalApi extends EnumEntry with EndpointSchema with Intern
}
object PluginInternalApi extends Enum[PluginInternalApi] with ApiModuleProvider[PluginInternalApi] {

case object UpdatePluginsIndex extends PluginInternalApi with ZeroParam with StartsAtVersion21 with SortIndex {
val z: Int = implicitly[Line].value
val description = "Update plugins index and licenses"
val (action, path) = POST / "pluginsinternal" / "update"
override def dataContainer: Option[String] = None
}
case object ListPlugins extends PluginInternalApi with ZeroParam with StartsAtVersion21 with SortIndex {
val z: Int = implicitly[Line].value
val description = "List all plugins"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,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 @@ -353,6 +353,43 @@ object RudderJsonResponse {
): LiftResponse = {
toLiftResponseOneEither(params, ResponseSchema.fromSchema(schema), SuccessIdTrace(id))(JsonEncoder[B], ev)
}

def toLiftResponseZeroEither(
params: DefaultParams,
schema: ResponseSchema,
id: IdTrace
)(implicit ev: A <:< Either[ResponseError, Any]): LiftResponse = {
implicit val prettify = params.prettify
result
.fold(
err => {
ApiLogger.ResponseError.info(err.fullMsg)
internalError(None, schema, err.fullMsg)
},
either => {
ev.apply(either) match {
case Left(e) =>
e match {
case UnauthorizedError(errorMsg) => unauthorizedError(id.error, schema, errorMsg)
}
case Right(_) =>
successZero(schema)
}
}
)
.runNow
}
def toLiftResponseZeroEither(params: DefaultParams, schema: EndpointSchema, id: Option[String])(implicit
ev: A <:< Either[ResponseError, Any]
): LiftResponse = {
toLiftResponseZeroEither(params, ResponseSchema.fromSchema(schema), ConstIdTrace(id))(ev)
}
def toLiftResponseZeroEither(params: DefaultParams, schema: EndpointSchema, id: A => Option[String])(implicit
ev: A <:< Either[ResponseError, Any]
): LiftResponse = {
toLiftResponseZeroEither(params, ResponseSchema.fromSchema(schema), SuccessIdTrace(id))(ev)
}

}
// when you don't have any response, just a success
implicit class ToLiftResponseZero(result: IOResult[Unit]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class PluginInternalApi(

def getLiftEndpoints(): List[LiftApiModule] = {
API.endpoints.map {
case API.UpdatePluginsIndex => UpdatePluginsIndex
case API.ListPlugins => ListPlugins
case API.InstallPlugins => InstallPlugins
case API.RemovePlugins => RemovePlugins
Expand All @@ -79,15 +80,28 @@ class PluginInternalApi(
implicit val encoder: JsonEncoder[PluginSettings] = DeriveJsonEncoder.gen[PluginSettings]
implicit val decoder: JsonDecoder[PluginSettings] = DeriveJsonDecoder.gen[PluginSettings]

object UpdatePluginsIndex extends LiftApiModule0 {
val schema: API.UpdatePluginsIndex.type = API.UpdatePluginsIndex
def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
pluginService
.updateIndex()
.chainError("Could not update plugins index")
.tapError(err => ApplicationLoggerPure.Plugin.error(err.fullMsg))
.map(_.map(err => RudderJsonResponse.UnauthorizedError(Some(err.msg))).toLeft(()))
.toLiftResponseZeroEither(params, schema, None)
}

}

object ListPlugins extends LiftApiModule0 {
val schema: API.ListPlugins.type = API.ListPlugins
def process0(version: ApiVersion, path: ApiPath, req: Req, params: DefaultParams, authzToken: AuthzToken): LiftResponse = {
pluginService
.list()
.chainError("Could not get plugins list")
.tapError(err => ApplicationLoggerPure.Plugin.error(err.fullMsg))
.map(_.left.map(c => RudderJsonResponse.UnauthorizedError(Some(c.fullMsg))).map(JsonPluginsSystemDetails.buildDetails))
.toLiftResponseOneEither[JsonPluginsSystemDetails](params, schema, None)
.map(JsonPluginsSystemDetails.buildDetails)
.toLiftResponseOne(params, schema, None)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ response:
"test-licensee"
],
"startDate" : "2025-01-10T21:53:20+01:00",
"endDate" : "2025-01-10T21:53:20+01:00",
"endDates" : {
"2025-01-10T21:53:20+01:00" : 1
},
"maxNodes" : 1000000
},
"plugins" : [
Expand Down Expand Up @@ -120,7 +122,9 @@ response:
"test-licensee"
],
"startDate" : "2025-01-10T21:53:20+01:00",
"endDate" : "2025-01-10T21:53:20+01:00",
"endDates" : {
"2025-01-10T21:53:20+01:00" : 1
},
"maxNodes" : 1000000
},
"plugins" : [
Expand Down Expand Up @@ -211,7 +215,9 @@ response:
"test-licensee"
],
"startDate" : "2025-01-10T21:53:20+01:00",
"endDate" : "2025-01-10T21:53:20+01:00",
"endDates" : {
"2025-01-10T21:53:20+01:00" : 1
},
"maxNodes" : 1000000
},
"plugins" : [
Expand Down Expand Up @@ -302,7 +308,9 @@ response:
"test-licensee"
],
"startDate" : "2025-01-10T21:53:20+01:00",
"endDate" : "2025-01-10T21:53:20+01:00",
"endDates" : {
"2025-01-10T21:53:20+01:00" : 1
},
"maxNodes" : 1000000
},
"plugins" : [
Expand Down Expand Up @@ -393,7 +401,9 @@ response:
"test-licensee"
],
"startDate" : "2025-01-10T21:53:20+01:00",
"endDate" : "2025-01-10T21:53:20+01:00",
"endDates" : {
"2025-01-10T21:53:20+01:00" : 1
},
"maxNodes" : 1000000
},
"plugins" : [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ update msg model =
ApiPostPlugins res ->
case res of
Ok t ->
( model, successNotification ("Plugin " ++ requestTypeText t ++ " successfull, the server should restart") )
( model, successNotification ("Plugin " ++ requestTypeText t ++ " successful") )

Err err ->
processApiErrorBytes "Error while fetching information" err model
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module Plugins.ApiCalls exposing (..)

import Http exposing (emptyBody, expectJson, header, request)
import Http exposing (emptyBody, header, request)
import Http.Detailed as Detailed
import Plugins.DataTypes exposing (..)
import Plugins.JsonDecoder exposing (decodeGetPluginsInfo)
Expand All @@ -12,6 +12,19 @@ getUrl m url =
m.contextPath ++ "/secure/api" ++ url


updateIndex : Model -> Cmd Msg
updateIndex model =
request
{ method = "POST"
, headers = [ header "X-Requested-With" "XMLHttpRequest" ]
, url = getUrl model "/pluginsinternal/update"
, body = emptyBody
, expect = Detailed.expectWhatever <| ApiPostPlugins << Result.map (\_ -> UpdateIndex)
, timeout = Nothing
, tracker = Nothing
}


getPluginInfos : Model -> Cmd Msg
getPluginInfos model =
request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import Time.ZonedDateTime exposing (ZonedDateTime)


type alias PluginsInfo =
{ license : LicenseGlobalInfo
{ license : Maybe LicenseGlobalInfo
, plugins : List PluginInfo
}


type alias LicenseGlobalInfo =
{ licensees : Maybe (List String)
, startDate : Maybe ZonedDateTime
, endDate : Maybe ZonedDateTime
, endDates : Maybe (List ( ZonedDateTime, Int ))
, maxNodes : Maybe Int
}

Expand Down Expand Up @@ -74,7 +74,7 @@ type alias UI =

type alias Model =
{ contextPath : String
, license : LicenseGlobalInfo
, license : Maybe LicenseGlobalInfo
, plugins : List PluginInfo
, ui : UI
}
Expand All @@ -92,6 +92,7 @@ type RequestType
| Uninstall
| Enable
| Disable
| UpdateIndex


type Msg
Expand Down Expand Up @@ -122,6 +123,9 @@ requestTypeText t =
Disable ->
"disable"

UpdateIndex ->
"index update"


processSelect : Select -> Model -> Model
processSelect select model =
Expand Down Expand Up @@ -168,6 +172,6 @@ noGlobalLicense : LicenseGlobalInfo
noGlobalLicense =
{ licensees = Nothing
, startDate = Nothing
, endDate = Nothing
, endDates = Nothing
, maxNodes = Nothing
}
Loading

0 comments on commit ffb8420

Please sign in to comment.