storage.h2.scala Maven / Gradle / Ivy
The newest version!
//: ----------------------------------------------------------------------------
//: Copyright (C) 2017 Verizon. All Rights Reserved.
//:
//: Licensed under the Apache License, Version 2.0 (the "License");
//: you may not use this file except in compliance with the License.
//: You may obtain a copy of the License at
//:
//: http://www.apache.org/licenses/LICENSE-2.0
//:
//: Unless required by applicable law or agreed to in writing, software
//: distributed under the License is distributed on an "AS IS" BASIS,
//: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//: See the License for the specific language governing permissions and
//: limitations under the License.
//:
//: ----------------------------------------------------------------------------
package nelson
package storage
import argonaut._
import java.net.URI
import scalaz._
import Scalaz._
import scalaz.concurrent.Task
import doobie.imports._
import journal._
import java.time.Instant
import scala.concurrent.duration.{FiniteDuration,MILLISECONDS}
final case class H2Storage(xa: Transactor[Task]) extends (StoreOp ~> Task) {
import StoreOp._
import Datacenter._
import nelson.audit.{AuditLog,AuditEvent,AuditAction,AuditCategory}
import Manifest.{Namespace => _, Port => _, TrafficShift => _, _}
val log = Logger[this.type]
override def apply[A](s: StoreOp[A]): Task[A] = s match {
case FindRepository(u, slug) => findRepository(u, slug).transact(xa)
case ListRepositories(u) => listRepositories(u).transact(xa)
case ListRepositoriesWithOwner(u, owner) => listRepositoriesWithOwner(u, owner).transact(xa)
case ListRepositoriesWithActiveHooks(u) => listRepositoriesWithActiveHooks(u).transact(xa)
case InsertOrUpdateRepositories(list) => insertOrUpdateRepositories(list).transact(xa)
case DeleteRepositories(list) => deleteRepositories(list).transact(xa)
case LinkRepositoriesToUser(list, u) => linkRepositoriesToUser(list, u).transact(xa)
case AddUnit(unit, repoId) => addUnit(unit, repoId).transact(xa)
case CreateRelease(repositoryId, r) => createRelease(repositoryId, r).transact(xa)
case KillRelease(slug, version) => killRelease(slug, version).transact(xa)
case ListRecentReleasesForRepository(slug) => listRecentReleasesForRepository(slug).transact(xa)
case ListReleases(limit) => listReleases(limit).transact(xa)
case FindRelease(id) => findRelease(id).transact(xa)
case CreateDatacenter(dc) => createDatacenter(dc).transact(xa)
case ListDatacenters => listDatacenters.transact(xa)
case ListNamespacesForDatacenter(dc) => listNamespacesForDatacenter(dc).transact(xa)
case ListDeploymentsForNamespaceByStatus(ns, stats, unit) => listDeploymentsForNamespaceByStatus(ns, stats, unit).transact(xa)
case GetNamespace(dc, nsName) => getNamespace(dc, nsName).transact(xa)
case GetNamespaceByID(ns) => getNamespaceByID(ns).transact(xa)
case CreateNamespace(dc, name) => createNamespace(dc, name).transact(xa)
case GetUnit(name, version) => getUnit(name, version).transact(xa)
case ListDeploymentsForUnitByStatus(nsid, name, s) => listDeploymentsForUnitByStatus(nsid, name, s).transact(xa)
case ListDeploymentStatuses(id) => listDeploymentStatuses(id).transact(xa)
case GetDeploymentStatus(id) => getDeploymentStatus(id).transact(xa)
case GetDeploymentResources(id) => getDeploymentResources(id).transact(xa)
case FindDeployment(stackName) => findDeployment(stackName).transact(xa)
case GetDeployment(id) => getDeployment(id).transact(xa)
case GetDeploymentByGuid(guid) => getDeploymentByGuid(guid).transact(xa)
case CreateDeployment(unitId, hash, namespace, wf, plan, policy) => createDeployment(unitId, hash, namespace, wf, plan, policy).transact(xa)
case CreateDeploymentStatus(id, status, msg) => createDeploymentStatus(id, status, msg).transact(xa)
case CreateManualDeployment(dc, ns, st, v, hash, desc, port, exp) => createManualDeployment(dc, ns, st, v, hash, desc, port, exp).transact(xa)
case ListUnitsByStatus(nsid, statuses) => listUnitsByStatus(nsid, statuses).transact(xa)
case FindReleaseByDeploymentGuid(guid) => findReleaseByDeploymentGuid(guid).transact(xa)
case FindReleasedByUnitNameAndVersion(u, v) => findReleasedByUnitNameAndVersion(u, v).transact(xa)
case CreateDeploymentExpiration(id, exp) => deleteAndThenCreateExpiration(id, exp).transact(xa)
case CreateDeploymentResource(dId, name, uri) => createDeploymentResource(dId, name, uri).transact(xa)
case FindDeploymentExpiration(id) => findDeploymentExpiration(id).transact(xa)
case ListShiftableDeployments(unit,ns) => listShiftableDeployments(unit,ns).transact(xa)
case GetDeploymentsForServiceNameByStatus(sn, nsid, s) => getDeploymentsForServiceNameByStatus(sn, nsid, s).transact(xa)
case GetCurrentTargetForServiceName(nsid, sn) => getCurrentTargetForServiceName(nsid, sn).transact(xa)
case CreateTrafficShift(nsid, to, policy, dur) => createTrafficShift(nsid, to, policy, dur).transact(xa)
case StartTrafficShift(from, to, start) => startTrafficShift(from, to, start).transact(xa)
case ReverseTrafficShift(id, reverse) => reverseTrafficShift(id, reverse).transact(xa)
case GetTrafficShiftForServiceName(nsid, sn) => getTrafficShiftForServiceName(nsid, sn).transact(xa)
case VerifyDeployable(dcName, nsName, unit) => verifyDeployable(dcName, nsName, unit).transact(xa)
case Audit(a) => audit(a).transact(xa)
case ListAuditLog(limit, offset, action, category) => listAuditLog(limit, offset, action, category).transact(xa)
case ListAuditLogByReleaseId(limit, offset, releaseId) => listAuditLogByReleaseId(limit, offset, releaseId).transact(xa)
case GetLoadbalancer(name, v) => getLoadbalancer(name, v).transact(xa)
case GetLoadbalancerDeployment(id) => getLoadbalancerDeployment(id).transact(xa)
case GetLoadbalancerDeploymentByGUID(id) => getLoadbalancerDeploymentByGUID(id).transact(xa)
case FindLoadbalancerDeployment(n,v,nsid) => findLoadbalancerDeployment(n,v,nsid).transact(xa)
case InsertLoadbalancerDeployment(lbid, nsid, hash, a) => insertLoadbalancerDeployment(lbid, nsid, hash, a).transact(xa)
case DeleteLoadbalancerDeployment(lbid) => deleteLoadbalancerDeployment(lbid).transact(xa)
case InsertLoadbalancerIfAbsent(lb, repoId) => insertLoadbalancerIfAbsent(lb, repoId).transact(xa)
case ListLoadbalancerDeploymentsForNamespace(nsid) => listLoadbalancerDeploymentsForNamespace(nsid).transact(xa)
case CountDeploymentsByStatus(since) => countDeploymentsByStatus(since).transact(xa)
case GetMostAndLeastDeployed(since, number, sortOrder) => getMostAndLeastDeployed(since, number, sortOrder).transact(xa)
case FindLastReleaseDeploymentStatus(s, u) => findLastReleaseDeploymentStatus(s, u).transact(xa)
case GetLatestReleaseForLoadbalancer(n, mv) => getLatestReleaseForLoadbalancer(n, mv).transact(xa)
}
implicit val metaVersion: Meta[Version] =
Meta[String].xmap({v => Version.fromString(v).yolo(s"metaVersion: could not extract version from $v")}, _.toString)
// TODO: remove the .get
implicit val metaFeatureVersion: Meta[FeatureVersion] =
Meta[String].xmap({v => FeatureVersion.fromString(v).yolo(s"metaFeatureVersion: could not extract feature version from $v")}, _.toString)
implicit val metaInstant: Meta[Instant] =
Meta[Long].xmap(Instant.ofEpochMilli, _.toEpochMilli)
implicit val metaFiniteDuration: Meta[FiniteDuration] =
Meta[Long].xmap(x => FiniteDuration(x, MILLISECONDS), _.toMillis)
implicit val metaMajorVersion: Meta[MajorVersion] =
Meta[Int].xmap(MajorVersion, _.major)
implicit val namespaceName: Meta[NamespaceName] =
Meta[String].xmap({name => NamespaceName.fromString(name).toOption.yolo(s"metaNamespaceName: could not extract namespace name from $name")}, _.asString)
type AuditRow = (ID, Instant, Option[Long], String, String, String, Option[String])
def listAuditLog(
limit: Long, offset: Long, action: Option[String], category: Option[String]
): ConnectionIO[List[AuditLog]] = {
val aLike = action.getOrElse("%")
val cLike = category.getOrElse("%")
val select = sql"""
SELECT id, timestamp, release_id, event, category, action, login
FROM PUBLIC.audit_log
WHERE action LIKE ${aLike}
AND category LIKE ${cLike}
ORDER BY timestamp DESC
LIMIT $limit
OFFSET $offset
"""
select
.query[AuditRow]
.process
.map(auditLogFromRow)
.list
}
def listAuditLogByReleaseId(limit: Long, offset: Long, releaseId: Long): ConnectionIO[List[AuditLog]] = {
val select = sql"""
SELECT id, timestamp, release_id, event, category, action, login
FROM PUBLIC.audit_log
WHERE release_id = $releaseId
ORDER BY timestamp DESC
LIMIT $limit
OFFSET $offset
"""
select
.query[AuditRow]
.process
.map(auditLogFromRow)
.list
}
def auditLogFromRow(row: AuditRow): AuditLog =
AuditLog(row._1, row._2, row._3, Parse.parseOption(row._4), row._5, row._6, row._7)
def audit[A](event: AuditEvent[A]): ConnectionIO[ID] = {
val json = event.auditable.encode(event.event)
val jsonStr = json.toString
val category = AuditCategory.stringify(event.auditable.category)
val action = AuditAction.stringify(event.action)
val insert = sql"""
INSERT INTO PUBLIC.audit_log (timestamp, event, release_id, category, action, login)
VALUES (${Instant.now()}, $jsonStr, ${event.releaseId}, $category, $action, ${event.userLogin})
"""
insert
.update
.withUniqueGeneratedKeys[ID]("id")
}
def createDatacenter(dc: Datacenter): ConnectionIO[Unit] =
sql"""MERGE INTO PUBLIC.datacenters (name)
VALUES (${dc.name})
""".update.run.map {
case 0 => log.info(s"[createDatacenter] adding ${dc.name} to datacenters (0)")
case x => log.debug(s"[createDatacenter] adding ${dc.name} to datacenters ($x)")
}
def listDatacenters: ConnectionIO[Set[String]] =
sql"""SELECT name
FROM PUBLIC.datacenters
ORDER BY name"""
.query[String]
.list.map(_.toSet)
type DeploymentRow = (ID, ID, ID, String, Instant, WorkflowRef, PlanRef, GUID, ExpirationPolicyRef)
/**
*
*/
def listDeploymentsForUnitByStatus(nsid: ID, name: UnitName, stats: NonEmptyList[DeploymentStatus]): ConnectionIO[Set[Deployment]] = {
val statusStrings = stats.map(_.toString)
implicit val statusesParam: Param[statusStrings.type] = Param.many(statusStrings)
val cio: ConnectionIO[List[Deployment]] = for {
dep <- sql"""SELECT d.id,
d.unit_id,
d.namespace_id,
d.hash,
d.deploy_time,
d.workflow,
d.plan,
d.guid,
d.expiration_policy
FROM PUBLIC.deployments d
JOIN PUBLIC.units u ON u.id = d.unit_id
LEFT JOIN PUBLIC.deployment_statuses AS ds
ON ds.deployment_id = d.id
AND ds.id = (
SELECT TOP 1 x.id
FROM PUBLIC.deployment_statuses AS x
WHERE x.deployment_id = d.id
ORDER BY x.id DESC
)
WHERE u.name = ${name}
AND d.namespace_id = ${nsid}
AND ds.state IN (${statusStrings: statusStrings.type})
""".query[DeploymentRow].process.list
dep2 <- dep.traverse(deploymentFromRow)
} yield dep2
cio.map(_.toSet).map(_ <| (d =>
log.debug(s"[listDeploymentsForUnitByStatus] found ${d.size} deployments with serviceType ${name} in namespace ${nsid}")))
}
def findDeployment(stackName: StackName): ConnectionIO[Option[Deployment]] = {
for {
dep <- sql"""SELECT d.id,
d.unit_id,
d.namespace_id,
d.hash,
d.deploy_time,
d.workflow,
d.plan,
d.guid,
d.expiration_policy
FROM PUBLIC.deployments d
JOIN PUBLIC.units u ON u.id = d.unit_id
WHERE u.name = ${stackName.serviceType}
AND u.version = ${stackName.version}
AND d.hash = ${stackName.hash}
""".query[DeploymentRow].option
dep2 <- dep.traverse(deploymentFromRow)
} yield dep2
}
/**
*
*/
def listDeploymentsForDatacenter(dc: String): ConnectionIO[Set[Deployment]] = {
val cio: ConnectionIO[List[Deployment]] = for {
dep <- sql"""SELECT id,
unit_id,
namespace_id,
hash,
deploy_time,
workflow,
plan,
guid,
expiration_policy
FROM PUBLIC.deployments
WHERE datacenter=${dc}
""".query[DeploymentRow].process.list
dep2 <- dep.traverse(deploymentFromRow)
} yield dep2
cio.map(_.toSet).map(_ <| (d => log.debug(s"[listDeploymentsForDatacenter] returning ${d.size} deployments for datacenter: ${dc}")))
}
def createDeploymentResource(deploymentId: ID, name: String, uri: java.net.URI): ConnectionIO[ID] =
sql"""INSERT INTO PUBLIC.deployment_resources (deployment_id, name, uri)
VALUES ($deploymentId, ${name}, ${uri.toString})
""".update
.withUniqueGeneratedKeys[ID]("id")
/**
*
*/
def createDeployment(unitId: ID, hash: String, namespace: Namespace, wf: WorkflowRef, plan: PlanRef, expPolicy: String): ConnectionIO[ID] =
sql"""INSERT INTO PUBLIC.deployments (unit_id, namespace_id, hash, deploy_time, workflow, plan, expiration_policy)
VALUES(${unitId}, ${namespace.id}, ${hash}, ${Instant.now()}, ${wf}, ${plan}, $expPolicy)
""".update
.withUniqueGeneratedKeys[ID]("id")
.map(_ <| (id => log.info(s"[createDeployment] created a deployment of unit: ${unitId} with hash ${hash} in namespace ${namespace.name}")))
/**
*
*/
def createDeploymentStatus(id: ID,
status: DeploymentStatus,
msg: Option[String]): ConnectionIO[Unit] =
sql"""
INSERT INTO PUBLIC.deployment_statuses
(deployment_id, state, status_msg, status_time)
VALUES (${id}, ${status.toString}, ${msg}, ${Instant.now()})
""".update.run.map(_ => log.info(s"[createDeploymentStatus] deployment: ${id} status: ${status} msg: ${msg}"))
def getDeploymentStatus(id: ID): ConnectionIO[Option[DeploymentStatus]] =
listDeploymentStatuses(id).map(_.headOption.map(_._1))
def getDeploymentResources(id: ID): ConnectionIO[Set[(String,java.net.URI)]] = {
sql"""
SELECT name, uri
FROM PUBLIC.deployment_resources
where deployment_id = $id
""".query[(String,String)].map(x => (x._1, new java.net.URI(x._2))).list.map(_.toSet)
}
/**
*
*/
def listDeploymentStatuses(id: ID): ConnectionIO[List[(DeploymentStatus, Option[StatusMessage], Instant)]] =
sql"""
SELECT
s.state,
s.status_msg,
s.status_time
FROM PUBLIC.deployment_statuses AS s
WHERE deployment_id = $id
ORDER BY s.deployment_id, s.status_time DESC
"""
.query[(String, Option[String], Instant)]
.process
.map {
case (state, msg, timestamp) =>
(DeploymentStatus.fromString(state), msg, timestamp)
}
.list
def getDeployment(id: ID): ConnectionIO[Deployment] = {
fetchDeployments(
sql"""SELECT id,
unit_id,
namespace_id,
hash,
deploy_time,
workflow,
plan,
guid,
expiration_policy
FROM PUBLIC.deployments
WHERE id = ${id}
ORDER BY id DESC
""".query[DeploymentRow].process.list).map(_.head)
}
def getDeploymentByGuid(guid: GUID): ConnectionIO[Option[Deployment]] =
fetchDeployments(
sql"""SELECT id,
unit_id,
namespace_id,
hash,
deploy_time,
workflow,
plan,
guid,
expiration_policy
FROM PUBLIC.deployments
WHERE guid = ${guid}
ORDER BY id DESC
""".query[DeploymentRow].process.list).map(_.headOption)
type DeploymentWithStatusRow = (ID, ID, ID, String, Instant, WorkflowRef, PlanRef, GUID, ExpirationPolicyRef, DeploymentStatusString)
def listDeploymentsForNamespaceByStatus(ns: ID, stats: NonEmptyList[DeploymentStatus], unit: Option[UnitName] = None): ConnectionIO[Set[(Deployment, DeploymentStatus)]] = {
// See http://tpolecat.github.io/doobie-0.2.3/05-Parameterized.html#dealing-with-in-clauses
val statusStrings = stats.map(_.toString)
implicit val statusesParam: Param[statusStrings.type] = Param.many(statusStrings)
def query1 = sql"""
SELECT
d.id,
d.unit_id,
d.namespace_id,
d.hash,
d.deploy_time,
d.workflow,
d.plan,
d.guid,
d.expiration_policy,
ds.state
FROM PUBLIC.deployments AS d
LEFT JOIN PUBLIC.deployment_statuses AS ds
ON ds.deployment_id = d.id
AND ds.id = (
SELECT TOP 1 x.id
FROM PUBLIC.deployment_statuses AS x
WHERE x.deployment_id = d.id
ORDER BY x.id DESC
)
WHERE namespace_id = ${ns}
AND ds.state IN (${statusStrings: statusStrings.type})
ORDER BY id DESC
"""
def query2(unitName: UnitName) = sql"""
SELECT
d.id,
d.unit_id,
d.namespace_id,
d.hash,
d.deploy_time,
d.workflow,
d.plan,
d.guid,
d.expiration_policy,
ds.state
FROM PUBLIC.deployments AS d
LEFT JOIN PUBLIC.deployment_statuses AS ds
ON ds.deployment_id = d.id
AND ds.id = (
SELECT TOP 1 x.id
FROM PUBLIC.deployment_statuses AS x
WHERE x.deployment_id = d.id
ORDER BY x.id DESC
)
JOIN PUBLIC.units AS u ON u.id = d.unit_id
WHERE u.name = ${unitName}
AND namespace_id = ${ns}
AND ds.state IN (${statusStrings: statusStrings.type})
ORDER BY id DESC
"""
val query = (unit match{
case Some(unitName) => query2(unitName)
case None => query1
}).query[DeploymentWithStatusRow].process.list
fetchDeploymentsWithStatuses(query)
}
def fetchDeployments(sql: ConnectionIO[List[DeploymentRow]]): ConnectionIO[Set[Deployment]] =
for {
dep <- sql
dep2 <- dep.traverse(deploymentFromRow).map(_.toSet)
} yield dep2.toSet
def fetchDeploymentsWithStatuses(sql: ConnectionIO[List[DeploymentWithStatusRow]]): ConnectionIO[Set[(Deployment,DeploymentStatus)]] =
for {
dep <- sql
dep2 <- dep.traverse(deploymentWithStatusFromRow).map(_.toSet)
} yield dep2.toSet
def deploymentFromRow(row: DeploymentRow): ConnectionIO[Deployment] =
for {
u <- getUnitById(row._2)
n <- getNamespaceByID(row._3)
} yield Deployment(row._1, u, row._4, n, row._5, row._6, row._7, row._8, row._9)
def deploymentWithStatusFromRow(row: DeploymentWithStatusRow): ConnectionIO[(Deployment, DeploymentStatus)] =
for {
u <- getUnitById(row._2)
n <- getNamespaceByID(row._3)
s = DeploymentStatus.fromString(row._10)
} yield (Deployment(row._1, u, row._4, n, row._5, row._6, row._7, row._8, row._9), s)
def listUnitsByStatus(
nsid: ID,
statuses: NonEmptyList[DeploymentStatus]
): ConnectionIO[Vector[(GUID,ServiceName)]] = {
// See http://tpolecat.github.io/doobie-0.2.3/05-Parameterized.html#dealing-with-in-clauses
val statusStrings = statuses.map(_.toString)
implicit val statusesParam: Param[statusStrings.type] = Param.many(statusStrings)
sql"""SELECT u.name, u.version, u.guid
FROM PUBLIC.units u
JOIN PUBLIC.deployments d
ON u.id = d.unit_id
JOIN PUBLIC.deployment_statuses s
ON s.deployment_id = d.id
WHERE d.namespace_id = $nsid
AND s.state IN (${statusStrings: statusStrings.type})
ORDER BY u.name ASC
""".query[(UnitName, Version, String)].map { case (name, version, guid) =>
(guid, ServiceName(name, version.toFeatureVersion))
}.vector.map(_.groupBy(_._2).values.map(_.head).toVector) // distinct by ServiceName
}
def createManualDeployment(datacenter: Datacenter,
namespace: NamespaceName,
serviceType: String,
version: String,
hash: String,
description: String,
port: Int,
exp: Instant): ConnectionIO[GUID] = {
def insertUnitIfAbsent: ConnectionIO[ID] = {
val insertUnit = sql"""
INSERT INTO PUBLIC.units
(description,
version,
name,
repository_id
)
VALUES (${description},
${version},
${serviceType},
-1
)
""".update.withUniqueGeneratedKeys[ID]("id")
val getUnitId = sql"""
SELECT u.id
FROM PUBLIC.units u
WHERE u.name = ${serviceType} AND u.version = ${version}
""".query[ID].option
getUnitId.flatMap(_.cata(
none = insertUnit,
some = id => id.point[ConnectionIO]
))
}
def insertRelease(v: String) = sql"""
MERGE INTO PUBLIC.releases (repository_id, version, timestamp, release_url, release_html_url)
KEY(repository_id, version)
VALUES(-1, ${v}, ${Instant.now()}, '', '')
""".update.run.map(x => log.debug(s"[insertRelease] merged ${x} rows creating a fake release for ${serviceType} version ${v} in namespace ${namespace.asString}"))
def insertPort(unit: ID) = sql"""
MERGE INTO PUBLIC.service_ports (unit, port, ref, protocol)
KEY(unit, ref)
VALUES(${unit}, ${port}, 'default', 'tcp')
""".update.run.map(_ => log.debug(s"[insertPort] inserting port: $port for unit $unit"))
for {
nsopt <- getNamespace(datacenter.name, namespace)
Some(ns) = nsopt // YOLO
_ <- insertRelease(version)
unit <- insertUnitIfAbsent
_ <- insertPort(unit)
id <- createDeployment(unit, hash, ns, "manual", Manifest.Plan.default.name, cleanup.RetainUntilDeprecated.name)
d <- getDeployment(id)
_ <- createDeploymentStatus(id, DeploymentStatus.Ready, Some(description))
_ <- createDeploymentExpiration(id, exp)
} yield d.guid
}
/*
* The only relevant expiration in the most recent so we actively
* prune the table. If we don't we could run into a situation
* where the table becomes too large for H2. Within a year of
* 1K deployments the table would contain around 8M rows.
* Delete previous expirations in the same transaction as creating new one.
*/
def deleteAndThenCreateExpiration(id: ID, exp: Instant): ConnectionIO[ID] =
deleteExpirations(id) *> createDeploymentExpiration(id, exp)
/*
* Deletes all expiration for a deployment
*/
def deleteExpirations(id: ID): ConnectionIO[Int] = {
val query = sql"""
DELETE FROM PUBLIC.deployment_expiration
WHERE deployment_id = $id
"""
query.update.run
}
def createDeploymentExpiration(id : ID, exp: Instant): ConnectionIO[ID] = {
val millis = exp.toEpochMilli
val insert = sql"""
INSERT INTO PUBLIC.deployment_expiration (deployment_id, expiration)
VALUES ($id, $millis)
"""
insert
.update
.withUniqueGeneratedKeys[ID]("id")
}
def findDeploymentExpiration(id: ID): ConnectionIO[Option[Instant]] = {
val select = sql"""
SELECT expiration
FROM PUBLIC.deployment_expiration
WHERE deployment_id = $id
ORDER BY id DESC
LIMIT 1
"""
select.query[Instant].option
}
def listShiftableDeployments(unit: Manifest.UnitDef, ns: ID): ConnectionIO[List[Deployment]] = {
listDeployments(unit.name, "%", ns, DeploymentStatus.routable)
}
def listNamespacesForDatacenter(dc: String): ConnectionIO[Set[Namespace]] = {
(for {
nsids <- sql"SELECT id, name FROM PUBLIC.namespaces WHERE datacenter=${dc}".query[(ID, NamespaceName)].list
nss = nsids.map {
case (nsid,nsname) => Namespace(nsid, nsname, dc)
}
} yield nss.toSet).map(_ <| (s => log.debug(s"""[listNamespacesForDatacenter] found namespaces: ${s.map(_.name).mkString("<",",",">")} in datacenter: $dc""")))
}
/**
* `datacenter` is the primary key and here acts as a forigen key
* for namespaces. We never want two datacenters with the same name.
*/
def createNamespace(dc: String, name: NamespaceName): ConnectionIO[ID] =
sql"""INSERT INTO PUBLIC.namespaces (datacenter, name)
VALUES (${dc}, ${name.asString})
""".update.withUniqueGeneratedKeys[ID]("id").map(_ <| (id => log.info(s"[createNamespace] created namespace ${name.asString} with id ${id} in datacenter ${dc}")))
def getNamespace(dc: String, nsName: NamespaceName): ConnectionIO[Option[Namespace]] =
(for {
nsids <- OptionT(
sql"""SELECT id, name
FROM PUBLIC.namespaces
WHERE name = ${nsName.asString}
AND datacenter = ${dc}""".query[(ID,NamespaceName)].option)
(id,name) = nsids
} yield Namespace(id, name, dc)).run.map(_ <| (ns => log.debug(s"[getNamespace] found ${ns.map(_.id)} as ${nsName.asString} namespace in dc: $dc")))
def getNamespaceByID(id: ID): ConnectionIO[Namespace] =
for {
nsids <- sql"""
SELECT id, name, datacenter
FROM PUBLIC.namespaces
WHERE id=${id}""".query[(ID,NamespaceName,String)].unique
(id,name,dc) = nsids
} yield Namespace(id, name, dc)
case class ReleaseRow(
slug: Slug,
version: Version,
timestamp: Instant,
releaseId: Long,
releaseHtmlUrl: URI,
unitName: String,
namespace: String,
hash: String,
deployid: Long,
deployTimestamp: Instant,
deployStatus: DeploymentStatus,
deployGuid: GUID
)
/**
* Fetch a single repository based on the supplied slug.
*/
def findRepository(u: User, slug: Slug): ConnectionIO[Option[Repo]] =
sql"""SELECT r.id, r.slug, u.access, r.hook_id, r.hook_is_active
FROM PUBLIC.repositories AS r
LEFT JOIN PUBLIC.user_repositories AS u
WHERE r.id = u.repository_id
AND u.login = ${u.login}
AND r.slug = ${slug.toString}
AND r.is_deleted = false
"""
.query[(Long,String,String,Option[Long],Option[Boolean])]
.process
.map {
case (id,slug,acc,Some(hid),Some(hactive)) =>
Repo(id,slug,acc,Option(Hook(hid,hactive))).toOption
case (id,slug,acc,_,_) =>
Repo(id,slug,acc,None).toOption
}
.list
.map(_.flatten)
.map(_.headOption).map( _ <| (r => log.debug(s"[findRepository] found ${r.map(_.id)} when looking for ${slug} for user: ${u.name}")))
/**
* Fetch all repositories for a given owner, that the specified user
* has access too.
*/
def listRepositoriesWithOwner(u: User, owner: String): ConnectionIO[List[Repo]] = {
val constraint = s"$owner/%"
sql"""SELECT r.id, r.slug, u.access, r.hook_id, r.hook_is_active
FROM PUBLIC.repositories AS r
LEFT OUTER JOIN PUBLIC.user_repositories AS u ON r.id = u.repository_id AND u.login = ${u.login}
WHERE slug LIKE $constraint
AND is_deleted = false"""
.query[(Long,String,Option[String],Option[Long],Option[Boolean])]
.process
.map {
case (id,slug,acc,Some(hid),hactive) =>
Repo(id,slug,acc.getOrElse("unknown"),Option(Hook(hid,hactive.getOrElse(false)))).toOption
case (id,slug,acc,None,hactive) =>
Repo(id,slug,acc.getOrElse("unknown"),None).toOption
}
.list
.map(_.flatten)
}
/**
* Fetch all repositories for a given user.
*/
def listRepositories(u: User): ConnectionIO[List[Repo]] =
listRepositoriesWithOwner(u, "%") // filthy hack
/**
* List all the repositories the user has access too that have
* webhooks enabled.
*/
def listRepositoriesWithActiveHooks(u: User): ConnectionIO[List[Repo]] = {
sql"""SELECT r.id, r.slug, u.access, r.hook_id, r.hook_is_active
FROM PUBLIC.repositories AS r
LEFT JOIN PUBLIC.user_repositories AS u
WHERE r.id = u.repository_id
AND u.login = ${u.login}
AND r.hook_is_active = true
AND r.is_deleted = false"""
.query[(Long,String,String,Option[Long],Boolean)]
.process
.map {
case (id,slug,acc,Some(hid),hactive) =>
Repo(id,slug,acc,Option(Hook(hid,hactive))).toOption
case (id,slug,acc,None,hactive) =>
Repo(id,slug,acc,None).toOption
}
.list
.map(_.flatten)
}
/**
* Given a set of repositories, associate them with a specific user.
* This is used when we are syncing repositories at the behence of a user.
*/
def linkRepositoriesToUser(list: List[Repo], u: User): ConnectionIO[Unit] = {
def statement(r: Repo): ConnectionIO[Int] =
sql"""MERGE INTO PUBLIC.user_repositories (repository_id, login, access)
KEY (repository_id, login)
VALUES (${r.id}, ${u.login}, ${r.access.toString})""".update.run
list.traverse(statement)
.map(_.sum)
.map(i => log.debug(s"merging $i rows into the existing user_repositories"))
}
/**
* Does what it says on the function.
*/
def insertOrUpdateRepositories(list: List[Repo]): ConnectionIO[Unit] = {
def statement(r: Repo): ConnectionIO[Int] =
sql"""MERGE INTO PUBLIC.repositories (id, slug, hook_id, hook_is_active, is_deleted)
KEY (id)
VALUES (${r.id}, ${r.slug.toString}, ${r.hook.map(_.id)}, ${r.hook.map(_.isActive).getOrElse(false)}, false);
""".update.run
list.traverse(statement)
.map(_.sum)
.map(i => log.debug(s"merging $i rows into the existing repository set"))
}
def deleteRepositories(repos: NonEmptyList[Repo]): ConnectionIO[Unit] = {
val repoIds = repos.map(_.id)
implicit val reposParam: Param[repoIds.type] = Param.many(repoIds)
// soft delete
val query = sql"""
UPDATE PUBLIC.repositories
SET is_deleted = true
WHERE id IN (${repoIds: repoIds.type})
"""
query.update.run.map(i => log.debug(s"deleting $repos from repositories"))
}
def killRelease(slug: Slug, version: String): ConnectionIO[Throwable \/ Unit] =
sql"""SELECT id FROM repositories WHERE slug=${slug.toString}""".query[Long].option flatMap {
case Some(r) => sql"""DELETE FROM releases WHERE repository_id=${r} and version=${version}""".update.run.as(\/.right(()))
case None => \/.left[Throwable, Unit](new NoSuchElementException(s"couldn't find a repo with slug: ${slug}")).point[ConnectionIO]
}
/**
*
*/
def createRelease(repositoryId: Long, r: Github.Release): ConnectionIO[Unit] = {
sql"""INSERT INTO PUBLIC.releases (repository_id, version, timestamp, release_id, release_url, release_html_url)
VALUES ( ${repositoryId}, ${r.tagName}, ${Instant.now()}, ${r.id}, ${r.url}, ${r.htmlUrl} );
""".update.run.void
}
def rowToReleased(row: (String,String,Instant,Long,String)): Option[Released] =
(Slug.fromString(row._1).toOption |@| Version.fromString(row._2)
)((a,b) => Released(a,b,row._3, row._4, new URI(row._5)))
// Given a unit name and version find the release event
def findReleasedByUnitNameAndVersion(un: UnitName, v: Version): ConnectionIO[Option[Released]] = {
val query = sql"""
SELECT
rr.slug,
r.version,
r.timestamp,
r.release_id,
r.release_html_url
FROM PUBLIC.units AS u
INNER JOIN PUBLIC.releases AS r
ON r.repository_id = u.repository_id
AND r.version = u.version
LEFT JOIN PUBLIC.repositories AS rr
ON rr.id = r.repository_id
WHERE u.name = $un AND u.version = ${v.toString}
LIMIT 1
""".query[(String,String,Instant,Long,String)]
query.map(rowToReleased).option.map(_.flatten)
}
def getLatestReleaseForLoadbalancer(name: String, mv: MajorVersion): ConnectionIO[Option[Released]] = {
val likeVersion = s"${mv}.%"
val query = sql"""
SELECT rr.slug, r.version, r.timestamp, r.release_id, release_html_url
FROM PUBLIC.loadbalancers as lb
INNER JOIN PUBLIC.releases as r
ON r.repository_id = lb.repository_id
AND r.version LIKE ${likeVersion}
LEFT JOIN PUBLIC.repositories AS rr
ON rr.id = r.repository_id
WHERE lb.name = $name AND lb.major_version = ${mv.toString}
ORDER BY r.release_id DESC
LIMIT 1
""".query[(String,String,Instant,Long,String)]
query.map(rowToReleased).option.map(_.flatten)
}
type ReleaseTuple = (String,String,Instant,Option[Long],Option[String],Option[String],String,String,Long,Instant,Option[String],GUID)
// TIM: major, major hack. tpolecat could not suggest better though,
// so for now, we go with this to avoid duplicating the query. Essentially
// this same SQL is used to list all releases (limited), list all releases
// for a given repo, and list a single specific release by version.
private def _listReleasesQuery(
slug: Option[Slug],
releaseId: Option[Long],
deploymentGuid: Option[GUID],
unitName: Option[UnitName] = None,
limit: Int = 50
): ConnectionIO[Released ==>> List[ReleasedDeployment]] = {
val base = s"""
SELECT
rr.slug,
r.version AS release_version,
r.timestamp,
r.release_id,
r.release_html_url,
u.name AS unit_name,
n.name AS namespace_name,
d.hash AS deployment_hash,
d.id AS deployment_id,
d.deploy_time AS deployment_time,
ds.state AS deployment_state,
d.guid
FROM PUBLIC.deployments AS d
INNER JOIN PUBLIC.namespaces AS n
ON n.id = d.namespace_id
INNER JOIN PUBLIC.units AS u
ON u.id = d.unit_id
INNER JOIN PUBLIC.releases AS r
ON r.repository_id = u.repository_id
AND r.version = u.version
LEFT JOIN PUBLIC.repositories AS rr
ON rr.id = r.repository_id
LEFT JOIN PUBLIC.deployment_statuses AS ds
ON ds.deployment_id = d.id
AND ds.id = (
SELECT TOP 1 x.id
FROM PUBLIC.deployment_statuses AS x
WHERE x.deployment_id = d.id
ORDER BY x.id DESC
)
"""
val constraint: Query0[ReleaseTuple] = (slug, releaseId, deploymentGuid, unitName) match {
case (Some(s), None, None, None) =>
Query[(String,Long), ReleaseTuple](s"""
$base
WHERE rr.slug IS ?
ORDER BY r.release_id DESC
LIMIT ?
""", None).toQuery0((s.toString, limit.toLong))
case (None, Some(id), None, None) =>
Query[(Long,Long), ReleaseTuple](s"""
$base
WHERE r.release_id IS ?
ORDER BY r.release_id DESC
LIMIT ?
""", None).toQuery0((id, limit.toLong))
case (None, None, Some(id), None) =>
Query[GUID, ReleaseTuple](s"""
$base
WHERE d.guid IS ?
LIMIT 1
""", None).toQuery0(id)
case (Some(s), None, None, Some(un)) =>
Query[(String,String), ReleaseTuple](s"""
$base
WHERE rr.slug IS ?
AND u.name = ?
ORDER BY r.release_id DESC
LIMIT 1
""", None).toQuery0((s.toString, un))
case _ =>
Query[Long, ReleaseTuple](
s"$base ORDER BY r.release_id DESC LIMIT ?"
).toQuery0(limit.toLong)
}
val result = constraint.process.map {
case (slug,ver,ts,Some(releaseid),Some(url),Some(uname),ns,hash,deployid,dts,state,dguid) =>
(Slug.fromString(slug).toOption |@|
Version.fromString(ver) |@|
state.map(DeploymentStatus.fromString(_))
)((a,b,c) => ReleaseRow(a, b, ts, releaseid, new URI(url), uname, ns, hash, deployid, dts, c, dguid))
case _ =>
log.error(s"unexpected non-result when querying for recent repo releases for '$slug'")
None // TIM: ZOMG so hacky. THROW AWAY ALL THE ERRORS
}
.list
.map(_.flatten)
val combined: ConnectionIO[Released ==>> List[ReleasedDeployment]] =
for {
a <- result
b <- a.traverse(row => getUnit(row.unitName, row.version).map{
case Some(unit) => {
val rel = Released(
slug = row.slug,
version = row.version,
timestamp = row.timestamp,
releaseId = row.releaseId,
releaseHtmlUrl = row.releaseHtmlUrl)
List(rel -> ReleasedDeployment(row.deployid, unit, row.namespace, row.hash, row.deployTimestamp, row.deployStatus, row.deployGuid))
}
case None => List.empty[(Released,ReleasedDeployment)]
})
} yield ==>>.fromListWith(b.flatten.map { case (r,d) => r -> List(d) })((y,z) => y ++ z)
combined
}
/**
*
*/
def findLastReleaseDeploymentStatus(r: Slug, u: UnitName): ConnectionIO[Option[DeploymentStatus]] =
_listReleasesQuery(
slug = Some(r),
releaseId = None,
deploymentGuid = None,
unitName = Some(u)).map(_.values.flatten.headOption.map(_.state))
/**
*
*/
def listReleases(limit: Int): ConnectionIO[Released ==>> List[ReleasedDeployment]] =
_listReleasesQuery(
slug = None,
releaseId = None,
deploymentGuid = None,
limit = limit)
/**
*
*/
def listRecentReleasesForRepository(slug: Slug): ConnectionIO[Released ==>> List[ReleasedDeployment]] =
_listReleasesQuery(
slug = Option(slug),
releaseId = None,
deploymentGuid = None,
limit = 50)
/**
*
*/
def findRelease(id: Long): ConnectionIO[Released ==>> List[ReleasedDeployment]] =
_listReleasesQuery(
slug = None,
releaseId = Option(id),
deploymentGuid = None,
limit = 20)
/**
* Given a deployment ID, find all the associated gubbins.
*/
def findReleaseByDeploymentGuid(guid: GUID): ConnectionIO[Option[(Released, ReleasedDeployment)]] =
_listReleasesQuery(
slug = None,
releaseId = None,
deploymentGuid = Option(guid),
limit = 20).map { pair =>
for {
(k,v) <- pair.findMax
o <- v.find(_.guid == guid)
} yield (k,o)
}
type TrafficShiftRow =
(String,Instant,FiniteDuration,Option[Instant],
ID, ID, ID, String, Instant, WorkflowRef, PlanRef, GUID, ExpirationPolicyRef,
ID, ID, ID, String, Instant, WorkflowRef, PlanRef, GUID, ExpirationPolicyRef)
private def trafficShiftFromRow(row: TrafficShiftRow): ConnectionIO[Datacenter.TrafficShift] = {
val (ref,st,dur,rev,
fid,fuid,fns,fhash,fdt,fwf,fp,fguid,fexp,
tid,tuid,tns,thash,tdt,twf,tp,tguid,texp) = row
for {
f <- deploymentFromRow((fid,fuid,fns,fhash,fdt,fwf,fp,fguid,fexp))
t <- deploymentFromRow((tid,tuid,tns,thash,tdt,twf,tp,tguid,texp))
b <- TrafficShiftPolicy.fromString(ref).yolo(s"can't parse traffic shift policy $ref").point[ConnectionIO]
} yield TrafficShift(f, t, b, st, dur, rev)
}
// Returns the most recent traffic shift for a given service name.
// Both from and to deployments must be in an routable state (i.e. ready or deprecated)
def getTrafficShiftForServiceName(ns: ID, sn: ServiceName): ConnectionIO[Option[TrafficShift]] = {
val statuses: NonEmptyList[String] = DeploymentStatus.routable.map(_.toString)
implicit val statusesParam: Param[statuses.type] = Param.many(statuses)
val likeVersion = s"${sn.version.major}.%"
val sql = sql"""
SELECT tb.policy, tbs.start_time, tb.duration, tbr.reverse_time,
fd.id, fu.id, fd.namespace_id, fd.hash, fd.deploy_time,fd.workflow, fd.plan, fd.guid, fd.expiration_policy,
td.id, tu.id, td.namespace_id, td.hash, td.deploy_time,td.workflow, td.plan, td.guid, td.expiration_policy
FROM PUBLIC.traffic_shifts tb
JOIN PUBLIC.traffic_shift_start tbs ON tbs.traffic_shift_id = tb.id
LEFT OUTER JOIN PUBLIC.traffic_shift_reverse tbr ON tbr.traffic_shift_id = tb.id
JOIN PUBLIC.deployments fd ON tbs.from_deployment = fd.id
JOIN PUBLIC.deployment_statuses fd_ds on fd_ds.deployment_id = fd.id
LEFT JOIN PUBLIC.deployment_statuses fd_ds2 on fd_ds2.deployment_id = fd.id and fd_ds2.status_time > fd_ds.status_time
JOIN PUBLIC.units fu on fd.unit_id = fu.id
JOIN PUBLIC.deployments td ON tb.to_deployment = td.id
JOIN PUBLIC.deployment_statuses td_ds on td_ds.deployment_id = td.id
LEFT JOIN PUBLIC.deployment_statuses td_ds2 on td_ds2.deployment_id = td.id and td_ds2.status_time > td_ds.status_time
JOIN PUBLIC.units tu on td.unit_id = tu.id
WHERE fu.name = ${sn.serviceType}
AND fu.version like ${likeVersion}
AND tb.namespace_id=${ns}
AND fd_ds.state IN (${statuses: statuses.type})
AND fd_ds2.deployment_id IS NULL
AND td_ds.state IN (${statuses: statuses.type})
AND td_ds2.deployment_id IS NULL
ORDER BY tbs.start_time DESC
LIMIT 1
""".query[TrafficShiftRow].option
sql.flatMap(_.traverse(trafficShiftFromRow))
}
private def listDeployments(name: UnitName, likeVersion: String, ns: ID, status: NonEmptyList[DeploymentStatus]): ConnectionIO[List[Deployment]] = {
val statuses = status.map(_.toString)
implicit val statusesParam: Param[statuses.type] = Param.many(statuses)
sql"""
SELECT d.id,
d.unit_id,
d.namespace_id,
d.hash,
d.deploy_time,
d.workflow,
d.plan,
d.guid,
d.expiration_policy
FROM PUBLIC.deployments d
JOIN PUBLIC.units u on d.unit_id = u.id
JOIN PUBLIC.deployment_statuses ds on ds.deployment_id = d.id
LEFT JOIN PUBLIC.deployment_statuses ds2 on ds2.deployment_id = d.id and ds2.status_time > ds.status_time
WHERE ds.state IN (${statuses: statuses.type})
AND d.namespace_id = ${ns}
AND u.name = $name
AND u.version LIKE $likeVersion
AND ds2.deployment_id IS NULL
""".query[DeploymentRow].list.flatMap(_.traverse(deploymentFromRow _))
}
/*
* Returns an order list of deployment by version and deployment time,
* within the version bounds provided by likeVersion and the given statuses.
*/
def getDeploymentsForServiceNameByStatus(sn: ServiceName, ns: ID, status: NonEmptyList[DeploymentStatus]): ConnectionIO[List[Deployment]] = {
val likeVersion: String = s"${sn.version.toMajorVersion}.%"
listDeployments(sn.serviceType, likeVersion, ns, status).map { deployments =>
val minVersion: Version = sn.version.minVersion
deployments
.filter(d => Order[Version].greaterThanOrEqual(d.unit.version, minVersion))
.sorted(Order[Deployment].toScalaOrdering.reverse)
}
}
/**
* This will first try to find a traffic shift, if none is found, it
* will try to find the latest matching deployment
*/
def getCurrentTargetForServiceName(ns: ID, sn: ServiceName): ConnectionIO[Option[Target]] =
getTrafficShiftForServiceName(ns, sn) flatMap {
case Some(t) if t.inProgress(Instant.now) =>
t.some.point[ConnectionIO].map(identity)
case Some(t) =>
SingletonTarget(t.deploymentTarget).some.point[ConnectionIO].map(identity)
case _ =>
getDeploymentsForServiceNameByStatus(sn, ns, DeploymentStatus.routable).map(_.headOption.map(SingletonTarget.apply))
}
def createTrafficShift(nsid: ID, to: Deployment, policy: TrafficShiftPolicy, dur: FiniteDuration): ConnectionIO[ID] = {
sql"""
INSERT INTO PUBLIC.traffic_shifts (
namespace_id,
to_deployment,
duration,
policy
) VALUES (
${nsid},
${to.id},
${dur.toMillis},
${policy.ref}
)
""".update.withUniqueGeneratedKeys[ID]("id")
}
/*
* Start a traffic shift given the to deployment's id.
* If traffic shift has already been started return same id,
* otherwise create new start
*/
def startTrafficShift(from: ID, to: ID, st: Instant): ConnectionIO[Option[ID]] = {
def getId: ConnectionIO[Option[ID]] =
sql"""
SELECT id
FROM PUBLIC.traffic_shifts
WHERE to_deployment = ${to}
""".query[ID].option
def getStart(tsid: ID): ConnectionIO[Option[ID]] =
sql"""
SELECT ID
FROM PUBLIC.traffic_shift_start
WHERE traffic_shift_id = ${tsid}
""".query[ID].option
def start(tsid: ID): ConnectionIO[ID] = {
val startTime = st.toEpochMilli
val insert =
sql"""
INSERT INTO PUBLIC.traffic_shift_start (traffic_shift_id, start_time, from_deployment)
VALUES (${tsid}, ${startTime}, ${from})
""".update.withUniqueGeneratedKeys[ID]("id")
getStart(tsid).flatMap(_.cata(
none = insert,
some = _.point[ConnectionIO]
))
}
(for {
tsid <- OptionT(getId)
id <- OptionT(start(tsid).map(Option(_)))
} yield id).run
}
/*
* Start a traffic shift reverse given the to deployment's id.
* If traffic reverse has already been started return same id,
* otherwise create new reverse
*/
def reverseTrafficShift(toID: ID, rev: Instant): ConnectionIO[Option[ID]] = {
def getId: ConnectionIO[Option[ID]] =
sql"""
SELECT id
FROM PUBLIC.traffic_shifts
WHERE to_deployment = ${toID}
""".query[ID].option
def getReverse(tsid: ID): ConnectionIO[Option[ID]] =
sql"""
SELECT ID
FROM PUBLIC.traffic_shift_reverse
WHERE traffic_shift_id = ${tsid}
""".query[ID].option
def reverse(tsid: ID): ConnectionIO[ID] = {
val reverseTime = rev.toEpochMilli
val insert =
sql"""
INSERT INTO PUBLIC.traffic_shift_reverse (traffic_shift_id, reverse_time)
VALUES (${tsid}, ${reverseTime})
""".update.withUniqueGeneratedKeys[ID]("id")
getReverse(tsid).flatMap(_.cata(
none = insert,
some = _.point[ConnectionIO]
))
}
(for {
tsid <- OptionT(getId)
id <- OptionT(reverse(tsid).map(Option(_)))
} yield id).run
}
def verifyDeployable(dcName: String, ns: NamespaceName, unit: Manifest.UnitDef): ConnectionIO[ValidationNel[NelsonError, Unit]] = {
def error(e: NelsonError): ConnectionIO[ValidationNel[NelsonError, Unit]] =
e.failureNel[Unit].point[ConnectionIO]
/*
* Try to resolve dependency is current namespace.
* If not resolved then move up namespace ancestory unil
* depedency is found or root namespace it hit.
*/
def resolveDependency(sn: ServiceName, ns: Datacenter.Namespace): ConnectionIO[ValidationNel[NelsonError, Unit]] = {
def hasDefaultPort(d: Deployment, sn: ServiceName): ConnectionIO[ValidationNel[NelsonError, Unit]] =
if (d.unit.ports.exists(_.name == "default"))
().successNel[NelsonError].point[ConnectionIO]
else
error(MissingDependency(unit.name, dcName, ns.name.asString, sn))
// dependencies must expose a default port and be in ready state
def isReady(d: Deployment, sn: ServiceName): ConnectionIO[ValidationNel[NelsonError, Unit]] =
getDeploymentStatus(d.id) flatMap {
case Some(DeploymentStatus.Ready) =>
().successNel[NelsonError].point[ConnectionIO]
case _ =>
error(DeprecatedDependency(unit.name, dcName, ns.name.asString, sn))
}
def checkMinVersion(d: Deployment, sn: ServiceName): ConnectionIO[ValidationNel[NelsonError, Unit]] =
if (d.unit.version.major == sn.version.major && d.unit.version.minor >= sn.version.minor)
().successNel[NelsonError].point[ConnectionIO]
else
error(MissingDependency(unit.name, dcName, ns.name.asString, sn))
def validateTarget(d: Deployment, sn: ServiceName): ConnectionIO[ValidationNel[NelsonError, Unit]] =
for {
a <- isReady(d, sn)
b <- hasDefaultPort(d, sn)
c <- checkMinVersion(d, sn)
} yield a +++ b +++ c
getCurrentTargetForServiceName(ns.id, sn) flatMap {
case Some(t) =>
t match {
case s: SingletonTarget =>
validateTarget(s.d, sn)
case t: TrafficShift =>
validateTarget(t.to, sn)
}
case None =>
// try to resolve dependency is namespace ancestory
ns.name.parent match {
case None => // this is a root namespace, dependency is missing
error(MissingDependency(unit.name, dcName, ns.name.asString, sn))
case Some(p) =>
getNamespace(dcName, p) flatMap {
case None =>
error(UnknownNamespace(dcName, p.asString))
case Some(n) =>
resolveDependency(sn, n)
}
}
}
}
getNamespace(dcName, ns) flatMap {
case None =>
error(UnknownNamespace(dcName, ns.asString))
case Some(n) =>
unit.dependencies.toVector.traverse { case (st,v) =>
resolveDependency(ServiceName(st,v), n)
}.map(_.foldMap(identity))
}
}
def addUnit(vunit: Manifest.UnitDef @@ Versioned, repo_id: ID): ConnectionIO[Unit] = {
val version = vunit.version
val unit = Versioned.unwrap(vunit)
def insertPort(unit: ID)(port: Manifest.Port): ConnectionIO[Unit] =
sql"""INSERT INTO PUBLIC.service_ports (unit, port, ref, protocol)
VALUES (${unit}, ${port.port}, ${port.ref}, ${port.protocol})
""".update.run.map(_ => log.debug(s"[insertPort] inserting port: $port for unit $unit"))
def insertServiceDependency(unitId: ID)(ds: (String,FeatureVersion)): ConnectionIO[Unit] =
sql"""INSERT INTO PUBLIC.unit_dependencies (from_unit, to_service, to_version)
VALUES (${unitId}, ${ds._1}, ${ds._2})
""".update.run.map(_ => log.debug(s"[insertServiceDependency] adding dependency $ds for unit: $unitId"))
def insertResource(unitId: ID)(rs: Manifest.Resource): ConnectionIO[Unit] =
sql"""INSERT INTO PUBLIC.unit_resources (unit_id, name)
VALUES (${unitId}, ${rs.name})
""".update.run.map(_ => log.debug(s"[insertResource] adding resource $rs for unit: $unitId"))
val insertUnit = sql"""
INSERT INTO PUBLIC.units (
description,
version,
name,
repository_id
)
VALUES (
${unit.description},
${version},
${unit.name},
${repo_id}
)
""".update.withUniqueGeneratedKeys[ID]("id")
for {
id <- insertUnit
_ <- unit.dependencies.toList.traverse_(insertServiceDependency(id))
_ <- unit.ports.traverse(_.nel.traverse_(insertPort(id)))
_ <- unit.resources.traverse_(insertResource(id))
} yield ()
}
def getUnitDependencies(id: ID): ConnectionIO[Set[ServiceName]] =
sql"""SELECT to_service, to_version FROM unit_dependencies
WHERE from_unit = ${id}
""".query[ServiceName].list.map(_.toSet)
def getUnitResources(id: ID): ConnectionIO[Set[String]] =
sql"""SELECT name
FROM PUBLIC.unit_resources
WHERE unit_id = ${id}
""".query[String].list.map(_.toSet)
def getUnitById(id: ID): ConnectionIO[DCUnit] = {
val res = for {
un <- sql"""SELECT name, version, description
FROM PUBLIC.units
WHERE id = $id""".query[(String, String, String)].unique
(n,v,desc) = un
deps <- getUnitDependencies(id)
ports <- unitPorts(id)
rs <- getUnitResources(id)
//TODO .get
dc = DCUnit(id, n, Version.fromString(v).yolo(s"getUnitById: could not extract version from string $v"), desc, deps, rs, ports)
} yield dc
res
}
def unitPorts(id: ID): ConnectionIO[Set[Port]] =
sql"SELECT port, ref, protocol FROM PUBLIC.service_ports WHERE unit = ${id}"
.query[Port].list.map(_.toSet)
def unitDependencies(id: ID): ConnectionIO[Set[ServiceName]] =
sql"""
SELECT ud.to_service, ud.to_version
FROM PUBLIC.units d
JOIN PUBLIC.unit_dependencies ud ON ud.to_service = d.name
WHERE ud.from_unit=${id}
""".query[(String,String)].map {
case (name,versionStr) => FeatureVersion.fromString(versionStr).map(ServiceName(name, _))
}.list.map(_.collect{ case Some(sn) => sn })
.map(_.toSet)
.map(_ <| (sn => log.debug(s"""[unitDependencies] found depedencies: ${sn.mkString("<",",",">")} for unit: $id""")))
def getUnit(name: String, version: Version): ConnectionIO[Option[DCUnit]] = {
val unitRowQ: ConnectionIO[Option[(ID, String, String, String)]] = sql"""
SELECT id, version, name, description
FROM PUBLIC.units
WHERE version = ${version.toString} AND name = ${name}
""".query[(ID, String, String, String)].option
val res: OptionT[ConnectionIO, DCUnit] =
for {
unitRow <- OptionT(unitRowQ)
(id,v,name,desc) = unitRow
deps <- unitDependencies(id).liftM[OptionT]
ports <- unitPorts(id).liftM[OptionT]
rs <- getUnitResources(id).liftM[OptionT]
} yield DCUnit(
id,
name,
Version.fromString(v).yolo(s"_getUnit: could not extract version from $v"),
desc,
deps,
rs,
ports
)
res.run
}
def countDeploymentsByStatus(since: Long): ConnectionIO[List[(String,Int)]] = {
val present = Instant.now.getEpochSecond * 1000
sql"""SELECT ds.state, COUNT(ds.state) FROM PUBLIC.deployments d
LEFT JOIN PUBLIC.deployment_statuses AS ds
ON ds.deployment_id = d.id
AND ds.id = (
SELECT TOP 1 x.id
FROM PUBLIC.deployment_statuses AS x
WHERE x.deployment_id = d.id
ORDER BY x.id DESC
)
WHERE d.deploy_time BETWEEN ${since} AND ${present}
GROUP BY ds.state
""".query[(String,Int)].list
}
def getMostAndLeastDeployed(since: Long, number: Int, sortOrder: String): ConnectionIO[List[(String,Int)]] = {
val sql =
s"""SELECT u.name, COUNT(u.name)
FROM PUBLIC.deployments d
LEFT JOIN PUBLIC.units AS u
ON u.id = d.unit_id
WHERE d.deploy_time BETWEEN ? AND ?
GROUP BY u.name
ORDER BY COUNT(u.name) ${sortOrder}
LIMIT ?
"""
val present = Instant.now.getEpochSecond * 1000
Query[(Long, Long, Int), (String, Int)](sql, None)
.toQuery0((since, present, number))
.list
}
def insertLoadbalancerDeployment(lbid: ID, nsid: ID, hash: String, address: String): ConnectionIO[ID] = {
sql"""
INSERT INTO PUBLIC.loadbalancer_deployments (loadbalancer_id, namespace_id, hash, address, deploy_time)
VALUES (${lbid}, ${nsid}, ${hash}, ${address}, ${Instant.now()})
""".update.withUniqueGeneratedKeys[ID]("id")
}
import Datacenter.{LoadbalancerDeployment, DCLoadbalancer}
import Manifest.{Route,BackendDestination,Port}
type RouteRow = (Int,String,String,String,String)
type LoadbalancerRow = (ID,ID,String,ID,String,MajorVersion,Instant,GUID,String)
def getLoadbalancerRoutes(id: ID): ConnectionIO[Vector[Manifest.Route]] = {
def rowToRoute(row: RouteRow): Manifest.Route =
Route(Port(row._2, row._1, row._3), BackendDestination(row._4,row._5))
sql"""
SELECT port, port_reference, protocol, to_unit_name, to_port_reference
FROM loadbalancer_routes
WHERE loadbalancer_id = ${id}
""".query[RouteRow].map(rowToRoute).vector
}
def getLoadbalancer(name: String, v: MajorVersion): ConnectionIO[Option[DCLoadbalancer]] = {
val query = sql"""
SELECT lb.id, lb.name, lb.major_version
FROM PUBLIC.loadbalancers lb
WHERE lb.name = ${name} AND lb.major_version = ${v}
""".query[(ID,String,MajorVersion)].option
(for {
lb <- OptionT(query)
rt <- getLoadbalancerRoutes(lb._1).liftM[OptionT]
} yield DCLoadbalancer(lb._1, lb._2, lb._3, rt)).run
}
private def loadbalancerRowToDeployment(lb: LoadbalancerRow): ConnectionIO[LoadbalancerDeployment] = {
getLoadbalancerRoutes(lb._4).map(rt =>
LoadbalancerDeployment(lb._1, lb._2, lb._3,
DCLoadbalancer(lb._4, lb._5, lb._6, rt), lb._7, lb._8, lb._9))
}
def findLoadbalancerDeployment(name: String, v: MajorVersion, nsid: ID): ConnectionIO[Option[LoadbalancerDeployment]] = {
val query: ConnectionIO[Option[LoadbalancerRow]] = sql"""
SELECT d.id, d.namespace_id, d.hash, lb.id, lb.name, lb.major_version, d.deploy_time, d.guid, d.address
FROM PUBLIC.loadbalancers lb
JOIN PUBLIC.loadbalancer_deployments d on lb.id = d.loadbalancer_id
WHERE lb.name = ${name}
AND lb.major_version = ${v}
AND d.namespace_id = ${nsid}
LIMIT 1
""".query[LoadbalancerRow].option
(for {
row <- OptionT(query)
lb <- loadbalancerRowToDeployment(row).liftM[OptionT]
} yield lb).run
}
def getLoadbalancerDeployment(id: ID): ConnectionIO[Option[LoadbalancerDeployment]] = {
val query = sql"""
SELECT d.id, d.namespace_id, d.hash, lb.id, lb.name, lb.major_version, d.deploy_time, d.guid, d.address
FROM PUBLIC.loadbalancers lb
JOIN PUBLIC.loadbalancer_deployments d on lb.id = d.loadbalancer_id
WHERE d.id = ${id}
""".query[LoadbalancerRow].option
(for {
row <- OptionT(query)
lb <- loadbalancerRowToDeployment(row).liftM[OptionT]
} yield lb).run
}
def getLoadbalancerDeploymentByGUID(guid: GUID): ConnectionIO[Option[LoadbalancerDeployment]] = {
val query = sql"""
SELECT d.id, d.namespace_id, d.hash, lb.id, lb.name, lb.major_version, d.deploy_time, d.guid, d.address
FROM PUBLIC.loadbalancers lb
JOIN PUBLIC.loadbalancer_deployments d on lb.id = d.loadbalancer_id
WHERE d.guid = ${guid}
""".query[LoadbalancerRow].option
(for {
row <- OptionT(query)
lb <- loadbalancerRowToDeployment(row).liftM[OptionT]
} yield lb).run
}
def listLoadbalancerDeploymentsForNamespace(nsid: ID): ConnectionIO[Vector[LoadbalancerDeployment]] = {
val query = sql"""
SELECT d.id, d.namespace_id, d.hash, lb.id, lb.name, lb.major_version, d.deploy_time, d.guid, d.address
FROM PUBLIC.loadbalancers lb
JOIN PUBLIC.loadbalancer_deployments d on lb.id = d.loadbalancer_id
WHERE d.namespace_id = ${nsid}
""".query[LoadbalancerRow].vector
for {
lbs <- query
rts <- lbs.traverse(loadbalancerRowToDeployment)
} yield rts
}
def insertLoadbalancerIfAbsent(lbv: Manifest.Loadbalancer @@ Versioned, repoId: ID): ConnectionIO[ID] = {
def getLoadbalancerId(name: String, version: MajorVersion) =
sql"""
SELECT lb.id
FROM PUBLIC.loadbalancers lb
WHERE lb.name = ${name} AND lb.major_version = ${version}
""".query[ID].option
val major = lbv.version.toMajorVersion
val lb = Manifest.Versioned.unwrap(lbv)
getLoadbalancerId(lb.name, major).flatMap(_.cata(
none = insertLoadbalancer(lb, major, repoId),
some = id => id.point[ConnectionIO]
))
}
def insertLoadbalancer(lb: Manifest.Loadbalancer, version: MajorVersion, repoId: ID): ConnectionIO[ID] = {
def insertRoutes(lbid: ID, routes: Vector[Manifest.Route]): ConnectionIO[Vector[ID]] = {
routes.traverse { r =>
sql"""
INSERT INTO PUBLIC.loadbalancer_routes (loadbalancer_id, port, port_reference, protocol, to_unit_name, to_port_reference)
VALUES (${lbid}, ${r.port.port}, ${r.port.ref}, ${r.port.protocol}, ${r.destination.name}, ${r.destination.portReference})
""".update.withUniqueGeneratedKeys[ID]("id")
}
}
def insertLB(name: String, version: MajorVersion) =
sql"""
INSERT INTO PUBLIC.loadbalancers (name, major_version, repository_id)
VALUES (${name}, ${version}, ${repoId})
""".update.withUniqueGeneratedKeys[ID]("id")
for {
id <- insertLB(lb.name, version)
_ <- insertRoutes(id, lb.routes)
} yield id
}
def deleteLoadbalancerDeployment(lbid: ID): ConnectionIO[Int] = {
val query = sql"""DELETE FROM PUBLIC.loadbalancer_deployments WHERE id = ${lbid}"""
query.update.run
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy