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 #3723 - Fix validation of duplicate volume names #3737
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -115,24 +115,39 @@ private[impl] object DVDIProviderValidations extends ExternalVolumeValidations { | |
// task instance across the entire cluster. | ||
override lazy val rootGroup = new Validator[Group] { | ||
override def apply(g: Group): Result = { | ||
val appsByVolume = | ||
val appsByVolume: Map[String, Set[PathId]] = | ||
g.transitiveApps | ||
.flatMap { app => namesOfMatchingVolumes(app).map(_ -> app.id) } | ||
.groupBy { case (volumeName, _) => volumeName } | ||
.mapValues(_.map { case (volumeName, appId) => appId }) | ||
|
||
val groupViolations = g.apps.flatMap { app => | ||
val ruleViolations = namesOfMatchingVolumes(app).flatMap { name => | ||
for { | ||
otherApp <- appsByVolume(name) | ||
if otherApp != app.id // do not compare to self | ||
} yield RuleViolation(app.id, s"Requested volume $name conflicts with a volume in app $otherApp", None) | ||
val appValid: Validator[AppDefinition] = { | ||
def volumeNameUnique(appId: PathId): Validator[ExternalVolume] = { | ||
def conflictingApps(vol: ExternalVolume): Set[PathId] = | ||
appsByVolume.getOrElse(vol.external.name, Set.empty).filter(_ != appId) | ||
|
||
isTrue { (vol: ExternalVolume) => | ||
val conflictingAppIds = conflictingApps(vol).mkString(", ") | ||
s"Volume name '${vol.external.name}' in $appId conflicts with volume(s) of same name in app(s): " + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's fix the path, and make the description simple. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure what is best there. I'll keep it "as is". We can improve it later. |
||
s"$conflictingAppIds" | ||
}{ vol => conflictingApps(vol).isEmpty } | ||
} | ||
|
||
validator[AppDefinition] { app => | ||
app.externalVolumes is every(volumeNameUnique(app.id)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To have the correct path here, you can do: app.externalVolumes as "container/volumes" is every(volumeNameUnique(app.id)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then the index is wrong Matthias Veit notifications@github.com schrieb am Di., 12. Apr. 2016
|
||
} | ||
if (ruleViolations.isEmpty) None | ||
else Some(GroupViolation(app, "app contains conflicting volumes", None, ruleViolations.toSet)) | ||
} | ||
group(groupViolations) | ||
|
||
def groupValid: Validator[Group] = validator[Group] { group => | ||
group.apps is every(appValid) | ||
group.groups is every(groupValid) | ||
} | ||
|
||
// We need to call the validators recursively such that the "description" of the rule violations | ||
// is correctly calculated. | ||
groupValid(g) | ||
} | ||
|
||
} | ||
|
||
override lazy val app = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package mesosphere.marathon.core.externalvolume.impl.providers | ||
|
||
import com.wix.accord.{ RuleViolation, Success, Failure, Result } | ||
import mesosphere.marathon.api.v2.Validation | ||
import mesosphere.marathon.core.externalvolume.ExternalVolumes | ||
import mesosphere.marathon.state._ | ||
import org.apache.mesos.{ Protos => MesosProtos } | ||
import org.scalatest.{ FunSuite, GivenWhenThen, Matchers } | ||
import play.api.libs.json.{ JsString, Json } | ||
|
||
import scala.collection.immutable.Seq | ||
|
||
class DVDIProviderRootGroupValidationTest extends FunSuite with Matchers with GivenWhenThen { | ||
|
||
test("two volumes with different names should not result in an error") { | ||
val f = new Fixture | ||
Given("a root group with two apps and conflicting volumes") | ||
val rootGroup = Group( | ||
id = PathId.empty, | ||
groups = Set( | ||
Group( | ||
id = PathId("/nested"), | ||
apps = Set( | ||
f.appWithDVDIVolume(appId = PathId("/nested/foo1"), volumeName = "vol1"), | ||
f.appWithDVDIVolume(appId = PathId("/nested/foo2"), volumeName = "vol2") | ||
) | ||
) | ||
) | ||
) | ||
|
||
f.checkResult( | ||
rootGroup, | ||
expectedViolations = Set.empty | ||
) | ||
} | ||
|
||
test("two volumes with same name result in an error") { | ||
val f = new Fixture | ||
Given("a root group with two apps and conflicting volumes") | ||
val app1 = f.appWithDVDIVolume(appId = PathId("/nested/app1"), volumeName = "vol") | ||
val app2 = f.appWithDVDIVolume(appId = PathId("/nested/app2"), volumeName = "vol") | ||
val rootGroup = Group( | ||
id = PathId.empty, | ||
groups = Set( | ||
Group( | ||
id = PathId("/nested"), | ||
apps = Set(app1, app2) | ||
) | ||
) | ||
) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed by #3751 |
||
f.checkResult( | ||
rootGroup, | ||
expectedViolations = Set( | ||
RuleViolation( | ||
value = app1.externalVolumes.head, | ||
constraint = "Volume name 'vol' in /nested/app1 conflicts with volume(s) of same name in app(s): /nested/app2", | ||
description = Some("/groups(0)/apps(0)/externalVolumes(0)") | ||
), | ||
RuleViolation( | ||
value = app2.externalVolumes.head, | ||
constraint = "Volume name 'vol' in /nested/app2 conflicts with volume(s) of same name in app(s): /nested/app1", | ||
description = Some("/groups(0)/apps(1)/externalVolumes(0)") | ||
) | ||
) | ||
) | ||
} | ||
|
||
class Fixture { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this a class instead of an object? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use a similar pattern elsewhere. If your |
||
def appWithDVDIVolume(appId: PathId, volumeName: String, provider: String = DVDIProvider.name): AppDefinition = { | ||
AppDefinition( | ||
id = appId, | ||
cmd = Some("sleep 123"), | ||
upgradeStrategy = UpgradeStrategy.forResidentTasks, | ||
container = Some( | ||
Container( | ||
`type` = MesosProtos.ContainerInfo.Type.MESOS, | ||
volumes = Seq( | ||
ExternalVolume( | ||
containerPath = "ignoreme", | ||
external = ExternalVolumeInfo( | ||
name = volumeName, | ||
provider = provider, | ||
options = Map( | ||
DVDIProvider.driverOption -> "rexray" | ||
) | ||
), | ||
mode = MesosProtos.Volume.Mode.RW | ||
) | ||
) | ||
) | ||
) | ||
) | ||
} | ||
|
||
def jsonResult(result: Result): String = { | ||
Json.prettyPrint( | ||
result match { | ||
case Success => JsString("Success") | ||
case f: Failure => Json.toJson(f)(Validation.failureWrites) | ||
} | ||
) | ||
} | ||
|
||
def checkResult(rootGroup: Group, expectedViolations: Set[RuleViolation]): Unit = { | ||
val expectFailure = expectedViolations.nonEmpty | ||
|
||
When("validating the root group") | ||
val result = DVDIProviderValidations.rootGroup(rootGroup) | ||
|
||
Then(s"we should ${if (expectFailure) "" else "NOT "}get an error") | ||
withClue(jsonResult(result)) { result.isFailure should be(expectFailure) } | ||
|
||
And("ExternalVolumes validation agrees") | ||
val externalVolumesResult = ExternalVolumes.validRootGroup()(rootGroup) | ||
withClue(jsonResult(externalVolumesResult)) { externalVolumesResult.isFailure should be(expectFailure) } | ||
|
||
And("global validation agrees") | ||
val globalResult: Result = Group.validRootGroup(maxApps = None)(rootGroup) | ||
withClue(jsonResult(globalResult)) { globalResult.isFailure should be(expectFailure) } | ||
|
||
val ruleViolations = globalResult match { | ||
case Success => Set.empty[RuleViolation] | ||
case f: Failure => f.violations.flatMap(Validation.allRuleViolationsWithFullDescription(_)) | ||
} | ||
|
||
And("the rule violations are the expected ones") | ||
withClue(jsonResult(globalResult)) { | ||
ruleViolations should equal(expectedViolations) | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
g.apps
instead ofg.transitiveApps
prevented validation of apps in nested groups.