Skip to content
This repository has been archived by the owner on Feb 8, 2019. It is now read-only.

Fixes #2881: Allow user to choose order of Directive application in a Rule #77

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/main/scala/com/normation/cfclerk/domain/Cf3PolicyDraft.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,47 @@ import net.liftweb.common._
*/
case class Cf3PolicyDraftId(value: String) extends HashcodeCaching


final case class BundleOrder(value: String)

object BundleOrder {
val default: BundleOrder = BundleOrder("")

/**
* Comparison logic for bundle: purelly alpha-numeric.
* The empty string come first.
* The comparison is stable, meaning that a sorted list
* with equals values stay in the same order after a sort.
*
* The sort is case insensitive.
*/
def compare(a: BundleOrder, b: BundleOrder): Int = {
String.CASE_INSENSITIVE_ORDER.compare(a.value, b.value)
}

def compareList(a: List[BundleOrder], b: List[BundleOrder]): Int = {

//only works on list of the same size
def compareListRec(a: List[BundleOrder], b: List[BundleOrder]): Int = {
(a, b) match {
case (ha :: ta, hb :: tb) =>
val comp = compare(ha,hb)
if(comp == 0) {
compareList(ta, tb)
} else {
comp
}
case _ => //we know they have the same size by construction, so it's a real equality
0
}
}

val maxSize = List(a.size, b.size).max
compareListRec(a.padTo(maxSize, BundleOrder.default), b.padTo(maxSize, BundleOrder.default))

}
}

/**
* That policy instance object is an instance of a policy applied (bound)
* to a particular node, so that its variable can be specialized given the node
Expand All @@ -70,6 +111,7 @@ final case class Cf3PolicyDraft(
, priority : Int
, serial : Int
, modificationDate: DateTime = DateTime.now
, order : List[BundleOrder]
) extends Loggable {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ class Cf3PromisesFileWriterServiceImpl(
val inputs = scala.collection.mutable.Buffer[String]() // all the include file

// Fetch the policies configured, with the system policies first
val policies = techniqueRepository.getByIds(container.getAllIds).sortWith((x,y) => x.isSystem)
val policies = sortTechniques(techniqueRepository.getByIds(container.getAllIds), container)

for {
tml <- policies.flatMap(p => p.templates)
Expand Down Expand Up @@ -361,6 +361,49 @@ class Cf3PromisesFileWriterServiceImpl(
)
}

/**
* Sort the techniques according to the order of the associated BundleOrder of Cf3PolicyDraft.
* Sort at best: sort rule then directives, and take techniques on that order, only one time
* Sort system directive first.
*/
private[this] def sortTechniques(techniques: Seq[Technique], container: Cf3PolicyDraftContainer): Seq[Technique] = {

def sortByOrder(tech: Seq[Technique], container: Cf3PolicyDraftContainer): Seq[Technique] = {
def compareBundleOrder(a: List[BundleOrder], b: List[BundleOrder]): Boolean = {
BundleOrder.compareList(a, b) <= 0
}
val drafts = container.getAll().values.toSeq

//for each technique, get it's best order from draft (if several directive use it) and return a pair (technique, List(order))
val pairs = tech.map { t =>
val tDrafts = drafts.filter { _.technique.id == t.id }.sortWith( (d1,d2) => compareBundleOrder(d1.order, d2.order))

//the order we want is the one with the lowest draft order, or the default one if no draft found (but that should not happen by construction)
val order = tDrafts.map( _.order ).headOption.getOrElse(List(BundleOrder.default))

(t, order)
}

//now just sort the pair by order and keep only techniques
val ordered = pairs.sortWith { case ((_, o1), (_, o2)) => BundleOrder.compareList(o1, o2) <= 0 }

//some debug info to understand what order was used for each node:
if(logger.isDebugEnabled) {
logger.debug(s"Sorted Technique for path [${container.outPath}] (and their Rules and Directives used to sort):")
ordered.map(p => s" `-> ${p._1.name}: [${p._2.map(_.value).mkString(" | ")}]").foreach { logger.debug(_) }
}

ordered.map( _._1 )
}

//system technique go first whatever their order
val (sys, user) = techniques.partition { _.isSystem }

sys ++ sortByOrder(user, container)

}


/**
* From the container, convert the parameter into StringTemplate variable, that contains a list of
* parameterName, parameterValue (really, the ParameterEntry itself)
Expand Down
105 changes: 105 additions & 0 deletions src/test/scala/com/normation/cfclerk/domain/BundleOrderTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
*************************************************************************************
* Copyright 2011 Normation SAS
*************************************************************************************
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero GPL v3, 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 Affero GPL v3
* licence, 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.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/agpl.html>.
*
*************************************************************************************
*/

package com.normation.cfclerk.domain

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




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

implicit def s2b(s: String): BundleOrder = BundleOrder(s)

implicit def l2b(l:List[String]): List[BundleOrder] = l.map(BundleOrder(_))


/**
* Reminder for my self-me that keep forgeting what compare means:
* compare(a,b) < 0 => sorted
* compare(a,b) > 0 => reverse order
*/

"Simple BundleOrder" should {

"be equal with emtpy srting" in {
BundleOrder.compare("", "") === 0
}

"let emtpy string be the smallest" in {
(BundleOrder.compare("plop", "") > 0) must beTrue
}

"use alphaNum" in {
(BundleOrder.compare("plop", "foo") > 0) must beTrue
}

"be sortable with numbers" in {
(BundleOrder.compare("050 a", "010 b") > 0) must beTrue
}

"be becarefull, it's NOT BY NUMBER" in {
(BundleOrder.compare("50", "100") > 0) must beTrue
}
}

"List compare" should {

"be equals with empty list" in {
BundleOrder.compareList(List(), List()) === 0
}

"ok for equals" in {
BundleOrder.compareList(List("10", "20"), List("10", "20")) === 0
}
"ok for equals, different size" in {
BundleOrder.compareList(List("10", ""), List("10")) === 0
}
"handle less than" in {
(BundleOrder.compareList(List("10", "100"), List("20")) < 0) must beTrue
}

"handle more than" in {
(BundleOrder.compareList(List("101", "100"), List("020", "200", "300")) > 0) must beTrue
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class Cf3PolicyDraftContainerServiceTest {
Map[String, Variable](variable1.spec.name -> variable1),
TrackerVariable(TrackerVariableSpec(), Seq()),
priority =0,
serial = 0
serial = 0, order = List()
)

policy2 = Cf3PolicyDraft(
Expand All @@ -87,7 +87,7 @@ class Cf3PolicyDraftContainerServiceTest {
Map[String, Variable](variable2.spec.name -> variable2, variable22.spec.name -> variable22),
TrackerVariable(TrackerVariableSpec(), Seq()),
priority =0,
serial = 0
serial = 0, order = List()
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ class DirectiveAgregationTest {

def createDirectiveWithBinding(activeTechniqueId:TechniqueId, i: Int): Cf3PolicyDraft = {
val instance = new Cf3PolicyDraft("id" + i, newTechnique(activeTechniqueId),
Map(), trackerVariable, priority = 0, serial = 0)
Map(), trackerVariable, priority = 0, serial = 0, order = List())

val variable = new InputVariable(InputVariableSpec("card", "varDescription1"), Seq("value" + i))
instance.copyWithAddedVariable(variable)
}

def createDirectiveWithArrayBinding(activeTechniqueId:TechniqueId, i: Int): Cf3PolicyDraft = {
val instance = new Cf3PolicyDraft("id" + i, newTechnique(activeTechniqueId), Map(), trackerVariable, priority = 0, serial = 0)
val instance = new Cf3PolicyDraft("id" + i, newTechnique(activeTechniqueId), Map(), trackerVariable, priority = 0, serial = 0, order = List())

val variable = InputVariable(
InputVariableSpec("card", "varDescription1", multivalued = true)
Expand All @@ -117,7 +117,7 @@ class DirectiveAgregationTest {
}

def createDirectiveWithArrayBindingAndNullValues(activeTechniqueId:TechniqueId, i: Int): Cf3PolicyDraft = {
val instance = new Cf3PolicyDraft("id" + i, newTechnique(activeTechniqueId), Map(), trackerVariable, priority = 0, serial = 0)
val instance = new Cf3PolicyDraft("id" + i, newTechnique(activeTechniqueId), Map(), trackerVariable, priority = 0, serial = 0, order = List())

val values = (0 until i).map(j =>
if (j > 0) "value" + i
Expand Down Expand Up @@ -166,7 +166,7 @@ class DirectiveAgregationTest {
val machineA = new Cf3PolicyDraftContainer("machineA", Set())

val instance = new Cf3PolicyDraft("id", newTechnique(TechniqueId(TechniqueName("name"), TechniqueVersion("1.0"))),
Map(), trackerVariable, priority = 0, serial = 0)
Map(), trackerVariable, priority = 0, serial = 0, order = List())
machineA.add(createDirectiveWithArrayBinding(activeTechniqueId1,1))
machineA.add(createDirectiveWithArrayBinding(activeTechniqueId1,2))

Expand Down