diff --git a/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala b/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala index 2230bc56010..032fd6e1da9 100644 --- a/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala +++ b/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala @@ -558,13 +558,34 @@ class ContainerProxy(factory: (TransactionId, val actionTimeout = job.action.limits.timeout.duration val (env, parameters) = ContainerProxy.partitionArguments(job.msg.content, job.msg.initArgs) + val environment = Map( + "namespace" -> job.msg.user.namespace.name.toJson, + "action_name" -> job.msg.action.qualifiedNameWithLeadingSlash.toJson, + "activation_id" -> job.msg.activationId.toString.toJson, + "transaction_id" -> job.msg.transid.id.toJson) + + // if the action requests the api key to be injected into the action context, add it here; + // treat a missing annotation as requesting the api key for backward compatibility + val authEnvironment = { + if (job.action.annotations.isTruthy(Annotations.ProvideApiKeyAnnotationName, valueForNonExistent = true)) { + job.msg.user.authkey.toEnvironment.fields + } else Map.empty + } + // Only initialize iff we haven't yet warmed the container val initialize = stateData match { case data: WarmedData => Future.successful(None) case _ => + val owEnv = (authEnvironment ++ environment + ("deadline" -> (Instant.now.toEpochMilli + actionTimeout.toMillis).toString.toJson)) map { + case (key, value) => "__OW_" + key.toUpperCase -> value + } + container - .initialize(job.action.containerInitializer(env), actionTimeout, job.action.limits.concurrency.maxConcurrent) + .initialize( + job.action.containerInitializer(env ++ owEnv), + actionTimeout, + job.action.limits.concurrency.maxConcurrent) .map(Some(_)) } @@ -575,29 +596,14 @@ class ContainerProxy(factory: (TransactionId, self ! InitCompleted(WarmedData(container, job.msg.user.namespace.name, job.action, Instant.now, 1)) } - // if the action requests the api key to be injected into the action context, add it here; - // treat a missing annotation as requesting the api key for backward compatibility - val authEnvironment = { - if (job.action.annotations.isTruthy(Annotations.ProvideApiKeyAnnotationName, valueForNonExistent = true)) { - job.msg.user.authkey.toEnvironment - } else JsObject.empty - } - - val environment = JsObject( - "namespace" -> job.msg.user.namespace.name.toJson, - "action_name" -> job.msg.action.qualifiedNameWithLeadingSlash.toJson, - "activation_id" -> job.msg.activationId.toString.toJson, - "transaction_id" -> job.msg.transid.id.toJson, + val env = authEnvironment ++ environment ++ Map( // compute deadline on invoker side avoids discrepancies inside container // but potentially under-estimates actual deadline "deadline" -> (Instant.now.toEpochMilli + actionTimeout.toMillis).toString.toJson) container - .run( - parameters, - JsObject(authEnvironment.fields ++ environment.fields), - actionTimeout, - job.action.limits.concurrency.maxConcurrent)(job.msg.transid) + .run(parameters, env.toJson.asJsObject, actionTimeout, job.action.limits.concurrency.maxConcurrent)( + job.msg.transid) .map { case (runInterval, response) => val initRunInterval = initInterval diff --git a/docs/actions-new.md b/docs/actions-new.md index ccfc95b401e..7a723247075 100644 --- a/docs/actions-new.md +++ b/docs/actions-new.md @@ -156,7 +156,13 @@ The initialization route is `/init`. It must accept a `POST` request with a JSON * `main` is the name of the function to execute. * `code` is either plain text or a base64 encoded string for binary functions (i.e., a compiled executable). * `binary` is false if `code` is in plain text, and true if `code` is base64 encoded. -* `env` is an map of key-value pairs of properties to export to the environment. +* `env` is a map of key-value pairs of properties to export to the environment. And contains several properties starting with the `__OW_` prefix that are specific to the running action. + * `__OW_API_KEY` the API key for the subject invoking the action, this key may be a restricted API key. This property is absent unless explicitly [requested](./annotations.md#annotations-for-all-actions). + * `__OW_NAMESPACE` the namespace for the _activation_ (this may not be the same as the namespace for the action). + * `__OW_ACTION_NAME` the fully qualified name of the running action. + * `__OW_ACTIVATION_ID` the activation id for this running action instance. + * `__OW_DEADLINE` the approximate time when this initializer will have consumed its entire duration quota (measured in epoch milliseconds). + The initialization route is called exactly once by the OpenWhisk platform, before executing a function. The route should report an error if called more than once. It is possible however that a single initialization diff --git a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala index 03927b51c7c..f5b6c7cc2d5 100644 --- a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala +++ b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala @@ -1416,11 +1416,36 @@ class ContainerProxyTests override def initialize(initializer: JsObject, timeout: FiniteDuration, concurrent: Int)( implicit transid: TransactionId): Future[Interval] = { initializeCount += 1 - initializer shouldBe action.containerInitializer { + + val envField = "env" + + (initializer.fields - envField) shouldBe (action.containerInitializer { activationArguments.fields.filter(k => filterEnvVar(k._1)) - } + }.fields - envField) timeout shouldBe action.limits.timeout.duration + val initializeEnv = initializer.fields(envField).asJsObject + + initializeEnv.fields("__OW_NAMESPACE") shouldBe invocationNamespace.name.toJson + initializeEnv.fields("__OW_ACTION_NAME") shouldBe message.action.qualifiedNameWithLeadingSlash.toJson + initializeEnv.fields("__OW_ACTIVATION_ID") shouldBe message.activationId.toJson + initializeEnv.fields("__OW_TRANSACTION_ID") shouldBe transid.id.toJson + + val convertedAuthKey = message.user.authkey.toEnvironment.fields.map(f => ("__OW_" + f._1.toUpperCase(), f._2)) + val authEnvironment = initializeEnv.fields.filterKeys(convertedAuthKey.contains) + if (apiKeyMustBePresent) { + convertedAuthKey shouldBe authEnvironment + } else { + authEnvironment shouldBe empty + } + + val deadline = Instant.ofEpochMilli(initializeEnv.fields("__OW_DEADLINE").convertTo[String].toLong) + val maxDeadline = Instant.now.plusMillis(timeout.toMillis) + + // The deadline should be in the future but must be smaller than or equal + // a freshly computed deadline, as they get computed slightly after each other + deadline should (be <= maxDeadline and be >= Instant.now) + initPromise.map(_.future).getOrElse(Future.successful(initInterval)) } override def run(parameters: JsObject, environment: JsObject, timeout: FiniteDuration, concurrent: Int)(