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 #20261: Add a warning in plugin page if a version mismatches rudder patch one #4085

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -50,13 +50,13 @@ import com.normation.rudder.web.model.CurrentUser
import com.normation.rudder.AuthorizationType
import com.normation.rudder.domain.logger.ApplicationLogger
import com.normation.eventlog.ModificationId
import java.util.Locale

import java.util.Locale
import net.liftweb.http.rest.RestHelper
import org.joda.time.DateTime
import com.normation.rudder.web.snippet.WithCachedResource
import java.net.URLConnection

import java.net.URLConnection
import com.normation.inventory.domain.InventoryProcessingLogger
import com.normation.plugins.AlwaysEnabledPluginStatus
import com.normation.plugins.RudderPluginModule
Expand All @@ -70,11 +70,12 @@ import com.normation.rudder.rest.EndpointSchema
import com.normation.rudder.rest.{InfoApi => InfoApiDef}
import com.normation.rudder.rest.lift.InfoApi
import com.normation.rudder.rest.lift.LiftApiModuleProvider

import net.liftweb.sitemap.Loc.LocGroup
import net.liftweb.sitemap.Loc.TestAccess
import org.reflections.Reflections
import com.normation.zio._

import com.normation.zio._
import scala.xml.NodeSeq
import scala.xml.NodeSeq.seqToNodeSeq

Expand Down
Expand Up @@ -67,18 +67,32 @@ trait DefaultPluginDef extends RudderPluginDef {

//get properties name for the plugin from "build.conf" file
//have default string for errors (and avoid "missing prop exception"):
lazy val defaults = List(
"plugin-name"
, "plugin-fullname"
, "plugin-title-description"
, "plugin-version"
).map(p => s"$p=missing property with name '$p' in file 'build.conf' for '${basePackage}'").mkString("\n")

//by convention, plugin "build.conf" file is copied into path:
lazy val defaults = {
val d1 = List(
"plugin-name"
, "plugin-fullname"
, "plugin-title-description"
, "plugin-version"
).map(p => s"$p=missing property with name '$p' in file 'build.conf' for '${basePackage}'").mkString("\n")

val d2 = List(
"branch-type"
, "rudder-version"
, "common-version"
, "private-version"
).map(p => s"$p=missing property with name '$p' in file 'main-build.conf' for '${basePackage}'").mkString("\n")

val res = d1 + "\n" + d2
res
}

//by convention, plugin "build.conf" and plugin-commons "main-build.conf" files are copied into path:
// target/classes/com/normation/plugins/${project.artifactId}
lazy val buildConfPath = basePackage.replaceAll("""\.""", "/") + "/build.conf"
lazy val mainBuildConfPath = basePackage.replaceAll("""\.""", "/") + "/main-build.conf"
lazy val buildConf = try {
ConfigFactory.load(this.getClass.getClassLoader, buildConfPath).withFallback(ConfigFactory.parseString(defaults))
val c1 = ConfigFactory.load(this.getClass.getClassLoader, buildConfPath).withFallback(ConfigFactory.parseString(defaults))
ConfigFactory.load(this.getClass.getClassLoader, mainBuildConfPath).withFallback(c1)
} catch {
case ex: ConfigException => //something want very wrong with "build.conf" parsing

Expand All @@ -90,11 +104,8 @@ trait DefaultPluginDef extends RudderPluginDef {
override lazy val shortName = buildConf.getString("plugin-name")
override lazy val displayName = buildConf.getString("plugin-title-description")
override lazy val version = {
val versionString = buildConf.getString("plugin-version")
PluginVersion.from(versionString).getOrElse(
//a version name that indicate an erro
PluginVersion(0,0,1, s"ERROR-PARSING-VERSION: ${versionString}")
)
val versionString = buildConf.getString("rudder-version") + "-" + buildConf.getString("plugin-version")
PluginVersion.from(versionString).getOrElse(PluginVersion.PARSING_ERROR(versionString))
}
override lazy val versionInfo = {
try {
Expand Down
Expand Up @@ -39,61 +39,81 @@ package com.normation.plugins

import scala.xml.NodeSeq
import com.normation.rudder.domain.logger.ApplicationLogger

import com.typesafe.config.{Config, ConfigFactory}
import bootstrap.liftweb.{ClassPathResource, ConfigResource, FileSystemResource, RudderProperties}
import com.normation.rudder.rest.EndpointSchema
import com.normation.rudder.rest.lift.LiftApiModuleProvider
import com.normation.utils._
import com.normation.utils.PartType._
import com.normation.utils.VersionPart._
import com.normation.utils.Separator.Dot
import com.normation.utils.Separator.Minus

import net.liftweb.sitemap.Menu

final case class PluginVersion(
major : Int
, minor : Int
, micro : Int
, prefix : String = ""
, suffix : String = ""
) {
final case class PluginVersion private (rudderAbi: Version, pluginVersion: Version) {

override def toString = prefix + major + "." + minor + "." + micro + suffix
override def toString = rudderAbi.toVersionStringNoEpoch + "-" + pluginVersion.toVersionString
}

object PluginVersion {


// a special value used to indicate a plugin version parsing error
def PARSING_ERROR(badVersion: String) = {
val vr = Version(0, Numeric(0), After(Dot, Numeric(0)) :: After(Dot, Numeric(1)) :: Nil)
val vp = Version(0, Numeric(0), After(Dot, Numeric(0)) :: After(Dot, Numeric(1)) :: After(Minus, Chars("ERROR-PARSING-VERSION: " + badVersion)) :: Nil)
new PluginVersion(vr, vp)
}

/*
* That method will create a plugin version from a string.
* It may fails if the pattern is not one known for rudder plugin, which is:
* (A.B-)x.y(.z)(post)
* (A.B(.C)(post1))-(x.y(.z)(post2))
* Where part between parenthesis are optionnal,
* A,B,x,y,z are non-empty list of digits,
* A.B are Rudder major.minor version,
* x.y are plugin major.minor.micro version - if micro not specified, assumed to be 0,
* post is any non blank/control char list (ex: ~alpha1)
* A.B(.C)(post1) are Rudder major.minor.patch version, with patch = 0 if not specified
* x.y(.z) are plugin major.minor.patch version - if patch not specified, assumed to be 0,
* postN is any non blank/control char list (ex: ~alpha1)
*
*/
def from(version: String): Option[PluginVersion] = {
//carefull: group matching nothing, like optionnal group, return null :(
def nonNull(s: String) = s match {
case null => ""
case x => x
}

val pattern = """(\d+\.\d+-)?(\d+)\.(\d+)(\.(\d+))?(\S+)?""".r.pattern
// the structure of our plugin is not really version-parsing friendly.
// We need to split on "-" which can also be a postfix separator. So
// we assume that there is always a digit just after the rudder/plugin "-" separator. And that the
// pattern "-digit" happens only one time.
val pattern = """(\S+)-(\d\S+)?""".r.pattern
val matcher = pattern.matcher(version)
if( matcher.matches ) {
val micro = matcher.group(5) match {
case null | "" => 0
case x => x.toInt

(ParseVersion.parse(matcher.group(1)), ParseVersion.parse(matcher.group(2))) match {
case (Right(rv), Right(pv)) => Some(PluginVersion(rv, pv))
case (x, y) => None
}
Some(PluginVersion(
matcher.group(2).toInt
, matcher.group(3).toInt
, micro
, nonNull(matcher.group(1))
, nonNull(matcher.group(6))
))

} else {
None
}
}

// normalize rudderVersion and pluginVersion to have at least 3 digits
def normalize(v: Version): Version = {
v match {
// at least 3 digits
case ok@Version(_, _, After(Dot, _:Numeric) :: After(Dot, _:Numeric) :: tail) => ok
// only 2 - add one 0
case Version(epoch, major, After(Dot, minor:Numeric) :: tail) =>
Version(epoch, major, After(Dot, minor) :: After(Dot, Numeric(0)) :: tail)
// only 1 - add two 0
case Version(epoch, major, tail) =>
Version(epoch, major, After(Dot, Numeric(0)) :: After(Dot, Numeric(0)) :: tail)
}
}

def apply(rudderVersion: Version, pluginVersion: Version) = {
new PluginVersion(normalize(rudderVersion), normalize(pluginVersion))
}
}

final case class PluginName(value:String) {
Expand Down Expand Up @@ -142,7 +162,12 @@ trait RudderPluginDef {
def description : NodeSeq

/**
* Version of the plugin.
* Full (i.e with Rudder version) version of the plugin.
* For example: 7.1.5-2.3.0 for a plugin in version 2.3.0 compile against rudder 7.1.5.
*
* It is composed of rudderAbi version: this is the version of rudder used to build the plugin
* (ie the 7.1.5 part in version example)
* And of plugin own version (ie the 2.3.0 part in version example).
*/
def version : PluginVersion

Expand All @@ -151,9 +176,6 @@ trait RudderPluginDef {
*/
def versionInfo : Option[String]




/*
* Information about the plugin activation status
* and license information.
Expand Down
Expand Up @@ -45,6 +45,7 @@ import scala.xml.NodeSeq
import com.normation.plugins.RudderPluginDef

import bootstrap.liftweb.PluginsInfo
import bootstrap.liftweb.RudderConfig

class PluginManagement extends DispatchSnippet with Loggable {

Expand All @@ -62,6 +63,14 @@ class PluginManagement extends DispatchSnippet with Loggable {
}

private[this] def displayPlugin(p:RudderPluginDef)(xml:NodeSeq) : NodeSeq = {
val rudderPluginVersion = p.version.rudderAbi.toVersionStringNoEpoch
// we compare on string, since we are just looking for an exact match.
val versionWarning = if(RudderConfig.rudderFullVersion != rudderPluginVersion) {
<span style="margin-left: 4px;" class="text-danger"><strong>
WARNING! This plugin was not build for current Rudder ABI version ({RudderConfig.rudderFullVersion}). You should update it to avoid code incompatibilities.
</strong></span>
} else NodeSeq.Empty

(
"data-plugin=name" #> {
p.versionInfo match {
Expand All @@ -76,7 +85,9 @@ class PluginManagement extends DispatchSnippet with Loggable {
} &
"data-plugin=fullid" #> p.name.value &
"data-plugin=baseclass" #> p.id &
"data-plugin=version" #> p.version.toString &
"data-plugin=version" #> p.version.pluginVersion.toVersionStringNoEpoch &
"data-plugin=rudderVersion" #> p.version.rudderAbi.toVersionStringNoEpoch &
"data-plugin=versionWarning" #> versionWarning &
"data-plugin=description" #> p.description &
"data-plugin=statusInformation" #> p.statusInformation()
)(xml)
Expand Down
Expand Up @@ -32,7 +32,10 @@ <h3 class="title-section" data-plugin="name">[Here comes the plugins name]</h3>
<p class="description" data-plugin="description">[Here comes the plugin description]</p>
<ul class="plugin-details">
<li><b>Plugin ID:</b> <span data-plugin="fullid">[Here comes the plugin full id]</span></li>
<li><b>Plugin version:</b> <span data-plugin="version">[Here comes the plugin version]</span></li>
<li><b>Plugin version:</b> <span data-plugin="version">[Here comes the plugin version]</li>
<li><b>Rudder ABI version:</b> <span data-plugin="rudderVersion">[Here comes the rudder version used by the plugin]</span>
<span data-plugin="versionWarning">[warning message if patch version of the plugin is not the same as Rudder]</span>
</li>
</ul>

<div class="plugin-description" class="pluginpadd">
Expand Down
Expand Up @@ -37,15 +37,24 @@

package com.normation.plugins

import com.normation.utils.ParseVersion

import org.junit.runner.RunWith
import org.specs2.mutable._
import org.specs2.runner.JUnitRunner

import com.normation.zio._
import org.joda.time.DateTime
import org.joda.time.format.ISODateTimeFormat

@RunWith(classOf[JUnitRunner])
class RudderPluginJsonTest extends Specification {
implicit class ForceParse(s: String) {
def toVersion = ParseVersion.parse(s) match {
case Left(err) => throw new IllegalArgumentException(s"Can not parse '${s}' as a version in test: ${err}")
case Right(v) => v
}
}

val index_json = """{
| "plugins": {
Expand Down Expand Up @@ -110,7 +119,7 @@ class RudderPluginJsonTest extends Specification {
val expected = List(
JsonPluginDef(
"rudder-plugin-branding"
, PluginVersion(1, 3, 0, "5.0-")
, PluginVersion("5.0.0".toVersion, "1.3.0".toVersion)
, List(
"/opt/rudder/share/plugins/",
"/opt/rudder/share/plugins/branding/",
Expand All @@ -122,7 +131,7 @@ class RudderPluginJsonTest extends Specification {
)
, JsonPluginDef(
"rudder-plugin-centreon"
, PluginVersion(1, 1, 0, "5.0-")
, PluginVersion("5.0.0".toVersion, "1.1.0".toVersion)
, List(
"/opt/rudder//",
"/opt/rudder//bin/",
Expand Down
@@ -0,0 +1,67 @@
/*
*************************************************************************************
* Copyright 2022 Normation SAS
*************************************************************************************
*
* This file is part of Rudder.
*
* Rudder is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In accordance with the terms of section 7 (7. Additional Terms.) of
* the GNU General Public License version 3, the copyright holders add
* the following Additional permissions:
* Notwithstanding to the terms of section 5 (5. Conveying Modified Source
* Versions) and 6 (6. Conveying Non-Source Forms.) of the GNU General
* Public License version 3, when you create a Related Module, this
* Related Module is not considered as a part of the work and may be
* distributed under the license agreement of your choice.
* A "Related Module" means a set of sources files including their
* documentation that, without modification of the Source Code, enables
* supplementary functions or services in addition to those offered by
* the Software.
*
* Rudder is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Rudder. If not, see <http://www.gnu.org/licenses/>.

*
*************************************************************************************
*/

package com.normation.plugins

import com.normation.utils.ParseVersion

import org.junit.runner.RunWith
import org.specs2.mutable._
import org.specs2.runner.JUnitRunner

@RunWith(classOf[JUnitRunner])
class RudderPluginTest extends Specification {

implicit class ForceParse(s: String) {
def toVersion = ParseVersion.parse(s) match {
case Left(err) => throw new IllegalArgumentException(s"Can not parse '${s}' as a version in test: ${err}")
case Right(v) => v
}
}

"Parsing a plugin version" should {
"be able to read simple rudder version" in {
PluginVersion.from("7.1.0-2.3.0") must_!= (PluginVersion("7.1.0".toVersion, "2.3.0".toVersion))
}
"automatically add a patch level (eq 0)" in {
PluginVersion.from("7.1-2.3") must_!= (PluginVersion("7.1.0".toVersion, "2.3.0".toVersion))
}
"understand complicated format with rc" in {
PluginVersion.from("7.0.0~rc2-SNAPSHOT-2.1-nightly") must_!= (PluginVersion("7.0.0~rc2-SNAPSHOT".toVersion, "2.1.0-nightly".toVersion))
}
}
}
Expand Up @@ -169,7 +169,8 @@ sealed trait VersionPart extends ToVersionString with Ordered[VersionPart] {

override def toVersionString: String = separator.toVersionString + value.toVersionString
override def compare(other: VersionPart): Int = VersionPart.compare(this, other)
}
}

object VersionPart {
final case class Before(separator: Separator, value: PartType) extends VersionPart //we can have before with "-" separator for ex with alpha, etc
final case class After (separator: Separator, value: PartType) extends VersionPart
Expand All @@ -181,7 +182,7 @@ object VersionPart {
val c = Separator.compare(a.separator, b.separator) // not sure? Does 1.0~alpha < 1.0.alpha ?
if(c == 0) PartType.compare(a.value, b.value) else c
}
}
}

object ParseVersion {
import fastparse._, NoWhitespace._
Expand Down