blob: 3acac54c08087347ea4ede1e3ab7b25f66e05fe6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.openwhisk.core.entity
import java.time.Instant
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.language.postfixOps
import scala.util.Try
import spray.json.DefaultJsonProtocol
import spray.json.DefaultJsonProtocol._
import spray.json._
import org.apache.openwhisk.common.TransactionId
import org.apache.openwhisk.core.database.{ArtifactStore, CacheChangeNotification, DocumentFactory}
import org.apache.openwhisk.core.entity.types.EntityStore
/**
* WhiskPackagePut is a restricted WhiskPackage view that eschews properties
* that are auto-assigned or derived from URI: namespace and name.
*/
case class WhiskPackagePut(binding: Option[Binding] = None,
parameters: Option[Parameters] = None,
version: Option[SemVer] = None,
publish: Option[Boolean] = None,
annotations: Option[Parameters] = None) {
/**
* Resolves the binding if it contains the default namespace.
*/
protected[core] def resolve(namespace: EntityName): WhiskPackagePut = {
WhiskPackagePut(binding.map(_.resolve(namespace)), parameters, version, publish, annotations)
}
}
/**
* A WhiskPackage provides an abstraction of the meta-data for a whisk package
* or package binding.
*
* The WhiskPackage object is used as a helper to adapt objects between
* the schema used by the database and the WhiskPackage abstraction.
*
* @param namespace the namespace for the action
* @param name the name of the action
* @param binding an optional binding, None for provider, Some for binding
* @param parameters the set of parameters to bind to the action environment
* @param version the semantic version
* @param publish true to share the action or false otherwise
* @param annotations the set of annotations to attribute to the package
* @param updated the timestamp when the package is updated
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
case class WhiskPackage(namespace: EntityPath,
override val name: EntityName,
binding: Option[Binding] = None,
parameters: Parameters = Parameters(),
version: SemVer = SemVer(),
publish: Boolean = false,
annotations: Parameters = Parameters(),
override val updated: Instant = WhiskEntity.currentMillis())
extends WhiskEntity(name, "package") {
require(binding != null || (binding map { _ != null } getOrElse true), "binding undefined")
/**
* Merges parameters into existing set of parameters for package.
* Existing parameters supersede those in p.
*/
def inherit(p: Parameters): WhiskPackage = copy(parameters = p ++ parameters).revision[WhiskPackage](rev)
/**
* Merges parameters into existing set of parameters for package.
* The parameters from p supersede parameters from this.
*/
def mergeParameters(p: Parameters): WhiskPackage = copy(parameters = parameters ++ p).revision[WhiskPackage](rev)
/**
* Gets the full path for the package.
* This is equivalent to calling this this.fullyQualifiedName(withVersion = false).fullPath.
*/
def fullPath: EntityPath = namespace.addPath(name)
/**
* Gets binding for package iff this is not already a package reference.
*/
def bind: Option[Binding] = if (binding.isEmpty) Some(Binding(namespace.root, name)) else None
/**
* Adds actions to package. The actions list is filtered so that only actions that
* match the package are included (must match package namespace/name).
*/
def withActions(actions: List[WhiskAction] = List.empty): WhiskPackageWithActions = {
withPackageActions(actions filter { a =>
val pkgns = binding map { b =>
b.namespace.addPath(b.name)
} getOrElse { namespace.addPath(name) }
a.namespace == pkgns
} map { a =>
WhiskPackageAction(a.name, a.version, a.annotations)
})
}
/**
* Adds package actions to package as actions or feeds. An action is considered a feed
* is it defined the property "feed" in the annotation. The value of the property is ignored
* for this check.
*/
def withPackageActions(actions: List[WhiskPackageAction] = List.empty): WhiskPackageWithActions = {
val (feedActions, nonFeedActions) = actions.partition(_.annotations.get(Parameters.Feed).isDefined)
WhiskPackageWithActions(this, nonFeedActions, feedActions)
}
def toJson = WhiskPackage.serdes.write(this).asJsObject
/**
* This the package summary as computed by the database view.
* Strictly used in view testing to enforce alignment.
*/
override def summaryAsJson = {
JsObject(
super.summaryAsJson.fields +
(WhiskPackage.bindingFieldName -> binding.map(Binding.serdes.write(_)).getOrElse(JsFalse)))
}
}
/**
* A specialized view of a whisk action contained in a package.
* Eschews fields that are implied by package in a GET package response..
*/
case class WhiskPackageAction(name: EntityName, version: SemVer, annotations: Parameters)
/**
* Extends WhiskPackage to include list of actions contained in package.
* This is used in GET package response.
*/
case class WhiskPackageWithActions(wp: WhiskPackage, actions: List[WhiskPackageAction], feeds: List[WhiskPackageAction])
object WhiskPackage
extends DocumentFactory[WhiskPackage]
with WhiskEntityQueries[WhiskPackage]
with DefaultJsonProtocol {
import WhiskActivation.instantSerdes
val bindingFieldName = "binding"
override val collectionName = "packages"
/**
* Traverses a binding recursively to find the root package and
* merges parameters along the way if mergeParameters flag is set.
*
* @param db the entity store containing packages
* @param pkg the package document id to start resolving
* @param mergeParameters flag that indicates whether parameters should be merged during package resolution
* @return the same package if there is no binding, or the actual reference package otherwise
*/
def resolveBinding(db: EntityStore, pkg: DocId, mergeParameters: Boolean = false)(
implicit ec: ExecutionContext,
transid: TransactionId): Future[WhiskPackage] = {
WhiskPackage.get(db, pkg) flatMap { wp =>
// if there is a binding resolve it
val resolved = wp.binding map { binding =>
if (mergeParameters) {
resolveBinding(db, binding.docid, true) map { resolvedPackage =>
resolvedPackage.mergeParameters(wp.parameters)
}
} else resolveBinding(db, binding.docid)
}
resolved getOrElse Future.successful(wp)
}
}
override implicit val serdes = {
/**
* Custom serdes for a binding - this property must be present in the datastore records for
* packages so that views can map over packages vs bindings.
*/
implicit val bindingOverride = new JsonFormat[Option[Binding]] {
override def write(b: Option[Binding]) = Binding.optionalBindingSerializer.write(b)
override def read(js: JsValue) = Binding.optionalBindingDeserializer.read(js)
}
jsonFormat8(WhiskPackage.apply)
}
override val cacheEnabled = true
lazy val publicPackagesView: View = WhiskQueries.entitiesView(collection = s"$collectionName-public")
// overriden to store encrypted parameters.
override def put[A >: WhiskPackage](db: ArtifactStore[A], doc: WhiskPackage, old: Option[WhiskPackage])(
implicit transid: TransactionId,
notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
super.put(
db,
doc.copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default)).revision[WhiskPackage](doc.rev),
old)
}
}
/**
* A package binding holds a reference to the providing package
* namespace and package name.
*/
case class Binding(namespace: EntityName, name: EntityName) {
def fullyQualifiedName = FullyQualifiedEntityName(namespace.toPath, name)
def docid = fullyQualifiedName.toDocId
override def toString = fullyQualifiedName.toString
/**
* Returns a Binding namespace if it is the default namespace
* to the given one, otherwise this is an identity.
*/
def resolve(ns: EntityName): Binding = {
namespace.toPath match {
case EntityPath.DEFAULT => Binding(ns, name)
case _ => this
}
}
}
object Binding extends ArgNormalizer[Binding] with DefaultJsonProtocol {
override protected[core] val serdes = jsonFormat2(Binding.apply)
protected[entity] val optionalBindingDeserializer = new JsonReader[Option[Binding]] {
override def read(js: JsValue) = {
if (js == JsObject.empty) None else Some(serdes.read(js))
}
}
protected[entity] val optionalBindingSerializer = new JsonWriter[Option[Binding]] {
override def write(b: Option[Binding]) = b match {
case None => JsObject.empty
case Some(n) => Binding.serdes.write(n)
}
}
}
object WhiskPackagePut extends DefaultJsonProtocol {
implicit val serdes = {
implicit val bindingSerdes = Binding.serdes
implicit val optionalBindingSerdes = new OptionFormat[Binding] {
override def read(js: JsValue) = Binding.optionalBindingDeserializer.read(js)
override def write(n: Option[Binding]) = Binding.optionalBindingSerializer.write(n)
}
jsonFormat5(WhiskPackagePut.apply)
}
}
object WhiskPackageAction extends DefaultJsonProtocol {
implicit val serdes = jsonFormat3(WhiskPackageAction.apply)
}
object WhiskPackageWithActions {
implicit val serdes = new RootJsonFormat[WhiskPackageWithActions] {
def write(w: WhiskPackageWithActions) = {
val JsObject(pkg) = WhiskPackage.serdes.write(w.wp)
JsObject(pkg + ("actions" -> w.actions.toJson) + ("feeds" -> w.feeds.toJson))
}
def read(value: JsValue) =
Try {
val pkg = WhiskPackage.serdes.read(value)
val actions = value.asJsObject.getFields("actions") match {
case Seq(JsArray(as)) =>
as map { a =>
WhiskPackageAction.serdes.read(a)
} toList
case _ => List.empty
}
val feeds = value.asJsObject.getFields("feeds") match {
case Seq(JsArray(as)) =>
as map { a =>
WhiskPackageAction.serdes.read(a)
} toList
case _ => List.empty
}
WhiskPackageWithActions(pkg, actions, feeds)
} getOrElse deserializationError("whisk package with actions malformed")
}
}