diff --git a/README.md b/README.md index 277ee5ad1..277586de1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,24 @@ This repository contains the code and configuration for the Nuvla API server, packaged as a Docker container. The API is inspired by the CIMI specification from DMTF. +## MEC Orchestrator (MEO) Positioning + +The Nuvla API Server functions as a **MEC Orchestrator (MEO)** as defined in +[ETSI GS MEC 003](https://www.etsi.org/deliver/etsi_gs/MEC/001_099/003/). +It provides system-level orchestration for edge applications across distributed +Multi-access Edge Computing (MEC) infrastructure. + +**Key MEC Capabilities:** +- **System Orchestration** - Multi-host application lifecycle management +- **Application Package Management** - On-boarding, validation, and distribution +- **Resource Management** - Placement decisions and infrastructure coordination +- **Standard Interfaces** - MEC-compliant APIs (Mm2, Mm3, Mm5, Mm9) + +For detailed MEC architecture mapping and implementation documentation, see: +- [MEC 003 Architectural Mapping](docs/5g-emerge/MEC-003-architectural-mapping.md) +- [MEC Terminology Guide](docs/5g-emerge/MEC-terminology-guide.md) +- [MEC Implementation Plans](docs/5g-emerge/) + ## Artifacts - `nuvla/api:`. A Docker container that can be obtained from diff --git a/code/project.clj b/code/project.clj index 9ab12a7d0..cf44eb30b 100644 --- a/code/project.clj +++ b/code/project.clj @@ -98,6 +98,7 @@ [org.testcontainers/testcontainers "1.20.4"] [peridot "0.5.4"] [clj-test-containers "0.7.4"] + [ring/ring-jetty-adapter "1.12.2"] [org.clojure/test.check "1.1.1"] [com.cemerick/url "0.1.1"] [org.clojars.konstan/kinsky-test-jar ~kinsky-version] diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/app_instance.clj b/code/src/com/sixsq/nuvla/server/resources/mec/app_instance.clj new file mode 100644 index 000000000..483b419c9 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/app_instance.clj @@ -0,0 +1,172 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-instance + "MEC 010-2 Application Instance Management (MEO Level) + + This namespace implements ETSI GS MEC 010-2 Application Lifecycle Management + APIs at the MEO (MEC Orchestrator) level. It provides a facade over Nuvla's + existing deployment resources to expose MEC-compliant endpoints. + + Scope: MEO-level orchestration only + Standard: ETSI GS MEC 010-2 v2.2.1" + (:require + [clojure.string :as str] + [clojure.tools.logging :as log] + [com.sixsq.nuvla.server.resources.deployment :as deployment] + [com.sixsq.nuvla.server.resources.module :as module] + [com.sixsq.nuvla.server.resources.nuvlabox :as nuvlabox] + [com.sixsq.nuvla.server.resources.spec.deployment :as deployment-spec] + [com.sixsq.nuvla.server.util.time :as time-utils])) + + +;; +;; State Mapping: Nuvla <-> MEC 010-2 +;; + +(def nuvla-to-mec-instantiation-state + "Maps Nuvla deployment states to MEC instantiation states" + {:CREATED :NOT_INSTANTIATED + :STARTING :INSTANTIATED + :STARTED :INSTANTIATED + :STOPPING :INSTANTIATED + :STOPPED :INSTANTIATED + :ERROR :INSTANTIATED + :PENDING :NOT_INSTANTIATED + :UNKNOWN :NOT_INSTANTIATED}) + + +(def nuvla-to-mec-operational-state + "Maps Nuvla deployment states to MEC operational states" + {:STARTED :STARTED + :STOPPED :STOPPED + :STARTING nil + :STOPPING nil + :ERROR nil + :CREATED nil + :PENDING nil + :UNKNOWN nil}) + + +(def mec-to-nuvla-state + "Reverse mapping for state translation" + {:NOT_INSTANTIATED :CREATED + :INSTANTIATED :STARTED}) + + +;; +;; Schema Definitions (MEC 010-2) +;; + +;; Note: Using simple predicates instead of specs for flexibility +;; MEC 010-2 data validation is done at the translation layer + +(defn valid-app-instance-id? [id] + (and (string? id) (str/starts-with? id "deployment/"))) + +(defn valid-app-d-id? [id] + (and (string? id) (str/starts-with? id "module/"))) + +(defn valid-instantiation-state? [state] + (contains? #{:NOT_INSTANTIATED :INSTANTIATED} (keyword state))) + +(defn valid-operational-state? [state] + (contains? #{:STARTED :STOPPED} (keyword state))) + + +;; +;; Translation Functions +;; + +(defn deployment->app-instance-info + "Translates a Nuvla deployment resource to MEC AppInstanceInfo" + [deployment] + (let [deployment-id (:id deployment) + module-id (:module deployment) + state (keyword (:state deployment)) + parent (:parent deployment) + instantiation (get nuvla-to-mec-instantiation-state state :NOT_INSTANTIATED) + operational (get nuvla-to-mec-operational-state state)] + (cond-> {:appInstanceId deployment-id + :appDId module-id + :instantiationState (name instantiation)} + + ;; Add appName from module if available + (:module/content deployment) + (assoc :appName (get-in deployment [:module/content :name])) + + ;; Add appProvider if available + (:module/author deployment) + (assoc :appProvider (:module/author deployment)) + + ;; Add operational state if applicable + operational + (assoc :operationalState (name operational)) + + ;; Add MEC host information if deployed + parent + (assoc :mecHostInformation {:hostId parent + :hostName parent}) + + ;; Add HATEOAS links + true + (assoc :_links {:self {:href (str "/app_lcm/v2/app_instances/" deployment-id)} + :instantiate {:href (str "/app_lcm/v2/app_instances/" deployment-id "/instantiate")} + :terminate {:href (str "/app_lcm/v2/app_instances/" deployment-id "/terminate")} + :operate {:href (str "/app_lcm/v2/app_instances/" deployment-id "/operate")}})))) + + +(defn app-instance-info->deployment + "Translates MEC AppInstanceInfo to Nuvla deployment resource (partial)" + [app-instance-info] + (let [instantiation-state (keyword (:instantiationState app-instance-info)) + nuvla-state (get mec-to-nuvla-state instantiation-state :CREATED)] + {:id (:appInstanceId app-instance-info) + :module (:appDId app-instance-info) + :state (name nuvla-state) + :parent (get-in app-instance-info [:mecHostInformation :hostId])})) + + +;; +;; Query Functions (placeholders for integration with Nuvla CRUD) +;; These will be implemented when integrated with the actual deployment resource +;; + +(comment + "Integration points with Nuvla deployment resource: + - get-app-instance: Call deployment CRUD read + - list-app-instances: Call deployment CRUD query + - create-app-instance: Call deployment CRUD create + - delete-app-instance: Call deployment CRUD delete") + + +;; +;; Validation Functions +;; + +(defn validate-app-instance-info + "Validates AppInstanceInfo against MEC 010-2 requirements" + [app-instance-info] + (when-not (:appInstanceId app-instance-info) + (throw (ex-info "appInstanceId is required" {:app-instance-info app-instance-info}))) + (when-not (:appDId app-instance-info) + (throw (ex-info "appDId is required" {:app-instance-info app-instance-info}))) + (when-not (:instantiationState app-instance-info) + (throw (ex-info "instantiationState is required" {:app-instance-info app-instance-info}))) + (when-not (valid-instantiation-state? (:instantiationState app-instance-info)) + (throw (ex-info "Invalid instantiationState" {:state (:instantiationState app-instance-info)}))) + app-instance-info) + + +;; +;; Lifecycle Hooks +;; + +(defn on-app-instance-created + "Hook called when an app instance is created" + [app-instance-info] + (log/info "MEC app instance created:" (:appInstanceId app-instance-info)) + app-instance-info) + + +(defn on-app-instance-deleted + "Hook called when an app instance is deleted" + [app-instance-id] + (log/info "MEC app instance deleted:" app-instance-id)) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_op_occ.clj b/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_op_occ.clj new file mode 100644 index 000000000..68a2d254c --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_op_occ.clj @@ -0,0 +1,171 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-lcm-op-occ + "MEC 010-2 Application Lifecycle Operation Occurrence Tracking + + Tracks lifecycle operations (instantiate, terminate, operate) and their status. + Maps to Nuvla's job resource with MEC-specific state tracking. + + Standard: ETSI GS MEC 010-2 v2.2.1 Section 6.2.3" + (:require + [clojure.string :as str] + [clojure.tools.logging :as log] + [com.sixsq.nuvla.server.resources.job :as job] + [com.sixsq.nuvla.server.util.time :as time-utils])) + + +;; +;; State Mapping: Nuvla Job <-> MEC Operation +;; + +(def nuvla-job-to-mec-operation-state + "Maps Nuvla job states to MEC operation states" + {:QUEUED :STARTING + :RUNNING :PROCESSING + :SUCCESS :COMPLETED + :FAILED :FAILED + :STOPPING :PROCESSING + :STOPPED :FAILED_TEMP + :CANCELED :ROLLED_BACK}) + + +(def mec-to-nuvla-job-state + "Reverse mapping for state translation" + {:STARTING :QUEUED + :PROCESSING :RUNNING + :COMPLETED :SUCCESS + :FAILED :FAILED + :FAILED_TEMP :STOPPED + :ROLLED_BACK :CANCELED}) + + +;; +;; Schema Definitions (using predicates for flexibility) +;; + +(defn valid-lcm-op-occ-id? [id] + (and (string? id) (str/starts-with? id "job/"))) + +(defn valid-operation-type? [op-type] + (contains? #{:INSTANTIATE :TERMINATE :OPERATE} (keyword op-type))) + +(defn valid-operation-state? [state] + (contains? #{:STARTING :PROCESSING :COMPLETED :FAILED :FAILED_TEMP :ROLLED_BACK} (keyword state))) + + +;; +;; Translation Functions +;; + +(defn job->app-lcm-op-occ + "Translates a Nuvla job to MEC AppLcmOpOcc" + [job] + (let [job-id (:id job) + job-state (keyword (:state job)) + operation-type (keyword (or (:operation-type job) "INSTANTIATE")) + mec-state (get nuvla-job-to-mec-operation-state job-state :STARTING) + target-id (:target-resource job)] + (cond-> {:lcmOpOccId job-id + :operationType (name operation-type) + :operationState (name mec-state) + :stateEnteredTime (or (:state-entered-time job) + (:updated job) + (time-utils/now-str)) + :startTime (or (:start-time job) + (:created job) + (time-utils/now-str)) + :appInstanceId target-id} + + ;; Add error information if job failed + (#{:FAILED :STOPPED} job-state) + (assoc :error {:type "about:blank" + :title "Operation Failed" + :status 500 + :detail (or (:status-message job) "Operation failed") + :instance job-id}) + + ;; Add HATEOAS links + true + (assoc :_links {:self {:href (str "/app_lcm/v2/app_lcm_op_occs/" job-id)} + :appInstance {:href (str "/app_lcm/v2/app_instances/" target-id)}})))) + + +(defn app-lcm-op-occ->job + "Translates MEC AppLcmOpOcc to Nuvla job (partial)" + [app-lcm-op-occ] + (let [operation-state (keyword (:operationState app-lcm-op-occ)) + nuvla-state (get mec-to-nuvla-job-state operation-state :QUEUED)] + {:id (:lcmOpOccId app-lcm-op-occ) + :state (name nuvla-state) + :operation-type (:operationType app-lcm-op-occ) + :target-resource (:appInstanceId app-lcm-op-occ) + :start-time (:startTime app-lcm-op-occ) + :state-entered-time (:stateEnteredTime app-lcm-op-occ)})) + + +;; +;; Query Functions (placeholders for integration with Nuvla CRUD) +;; These will be implemented when integrated with the actual job resource +;; + +(comment + "Integration points with Nuvla job resource: + - get-app-lcm-op-occ: Call job CRUD read + - list-app-lcm-op-occs: Call job CRUD query with filters + - create-app-lcm-op-occ: Call job CRUD create + - update-operation-state: Call job CRUD update") + + +;; +;; Operation State Transitions (placeholders) +;; + +(comment + "State transition functions to be implemented when integrated with job resource: + - update-operation-state: Updates job state + - complete-operation: Marks job as SUCCESS + - fail-operation: Marks job as FAILED") + + +;; +;; Validation +;; + +(defn validate-app-lcm-op-occ + "Validates AppLcmOpOcc against MEC 010-2 requirements" + [app-lcm-op-occ] + (when-not (:lcmOpOccId app-lcm-op-occ) + (throw (ex-info "lcmOpOccId is required" {:app-lcm-op-occ app-lcm-op-occ}))) + (when-not (:operationType app-lcm-op-occ) + (throw (ex-info "operationType is required" {:app-lcm-op-occ app-lcm-op-occ}))) + (when-not (:operationState app-lcm-op-occ) + (throw (ex-info "operationState is required" {:app-lcm-op-occ app-lcm-op-occ}))) + (when-not (valid-operation-type? (:operationType app-lcm-op-occ)) + (throw (ex-info "Invalid operationType" {:type (:operationType app-lcm-op-occ)}))) + (when-not (valid-operation-state? (:operationState app-lcm-op-occ)) + (throw (ex-info "Invalid operationState" {:state (:operationState app-lcm-op-occ)}))) + app-lcm-op-occ) + + +;; +;; Lifecycle Hooks +;; + +(defn on-operation-started + "Hook called when an operation starts" + [app-lcm-op-occ] + (log/info "MEC operation started:" + (:operationType app-lcm-op-occ) + "for app instance" + (:appInstanceId app-lcm-op-occ)) + app-lcm-op-occ) + + +(defn on-operation-completed + "Hook called when an operation completes" + [lcm-op-occ-id] + (log/info "MEC operation completed:" lcm-op-occ-id)) + + +(defn on-operation-failed + "Hook called when an operation fails" + [lcm-op-occ-id error-detail] + (log/error "MEC operation failed:" lcm-op-occ-id error-detail)) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_op_tracking.clj b/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_op_tracking.clj new file mode 100644 index 000000000..6c6070163 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_op_tracking.clj @@ -0,0 +1,326 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-lcm-op-tracking + "MEC 010-2 Application Lifecycle Management Operation Tracking + + This module provides job-based tracking for AppLcmOpOcc (Application Lifecycle + Management Operation Occurrences). It integrates with Nuvla's job system to: + + - Create job resources for each lifecycle operation + - Synchronize state between jobs and AppLcmOpOcc + - Provide operation history queries + - Track operation progress and completion + + Standard: ETSI GS MEC 010-2 v2.2.1 + Section: 6.2.3 AppLcmOpOcc (Application LCM Operation Occurrence)" + (:require + [clojure.tools.logging :as log] + [com.sixsq.nuvla.server.resources.job :as job] + [com.sixsq.nuvla.server.resources.mec.app-lcm-op-occ :as app-lcm-op-occ] + [com.sixsq.nuvla.server.util.time :as time-utils])) + + +;; +;; Job Creation for Lifecycle Operations +;; + +(defn create-operation-job + "Create a job resource for tracking a lifecycle operation. + + Parameters: + - operation-type: Type of operation (:INSTANTIATE, :TERMINATE, :OPERATE) + - app-instance-id: ID of the application instance + - request-params: Operation-specific parameters + - user-id: ID of the user initiating the operation + + Returns: + - Job resource map with operation tracking metadata" + [operation-type app-instance-id request-params user-id] + (log/info "Creating operation job for" operation-type "on" app-instance-id) + (let [job-name (str (name operation-type) " - " app-instance-id) + job-id (str "job/" (java.util.UUID/randomUUID)) + now (time-utils/now-str)] + {:id job-id + :resource-type "job" + :name job-name + :description (str "MEC Application Lifecycle Operation: " (name operation-type)) + :action (name operation-type) + :target-resource app-instance-id + :state "QUEUED" + :progress 0 + :status-message "Operation queued" + :created now + :updated now + :acl {:owners [user-id] + :view-data [user-id]} + + ;; MEC-specific metadata + :mec-operation-type (name operation-type) + :mec-app-instance-id app-instance-id + :mec-request-params request-params + + ;; Timestamps + :start-time now + :state-entered-time now})) + + +(defn start-operation-job + "Transition job to RUNNING state. + + Parameters: + - job-id: ID of the job to start + + Returns: + - Updated job map" + [job-id] + (log/info "Starting operation job" job-id) + (let [now (time-utils/now-str)] + {:id job-id + :state "RUNNING" + :progress 10 + :status-message "Operation in progress" + :updated now + :state-entered-time now})) + + +(defn complete-operation-job + "Mark job as successfully completed. + + Parameters: + - job-id: ID of the job to complete + - result: Operation result data + + Returns: + - Updated job map" + [job-id result] + (log/info "Completing operation job" job-id) + (let [now (time-utils/now-str)] + {:id job-id + :state "SUCCESS" + :progress 100 + :status-message "Operation completed successfully" + :return-code 0 + :result result + :updated now + :state-entered-time now + :time-of-status-change now})) + + +(defn fail-operation-job + "Mark job as failed. + + Parameters: + - job-id: ID of the job to fail + - error-detail: Error information (map with :title, :detail, :status) + + Returns: + - Updated job map" + [job-id error-detail] + (log/error "Failing operation job" job-id "with error:" (:detail error-detail)) + (let [now (time-utils/now-str)] + {:id job-id + :state "FAILED" + :progress 0 + :status-message (:detail error-detail) + :return-code 1 + :error error-detail + :updated now + :state-entered-time now + :time-of-status-change now})) + + +;; +;; State Synchronization +;; + +(defn job->app-lcm-op-occ + "Convert a job resource to an AppLcmOpOcc representation. + Delegates to app-lcm-op-occ namespace for the actual conversion. + + Parameters: + - job: Job resource map + + Returns: + - AppLcmOpOcc map conforming to MEC 010-2 schema" + [job] + (app-lcm-op-occ/job->app-lcm-op-occ job)) + + +(defn get-operation-state + "Get the current AppLcmOpOcc state for a job. + + Parameters: + - job-id: ID of the job + + Returns: + - AppLcmOpOcc map or nil if job not found" + [job-id] + (log/debug "Getting operation state for job" job-id) + ;; In a real implementation, this would query the job resource + ;; For now, we return a placeholder + (when job-id + (let [job {:id job-id + :state "RUNNING" + :operation-type "INSTANTIATE" + :target-resource "deployment/test-123" + :start-time (time-utils/now-str) + :state-entered-time (time-utils/now-str)}] + (job->app-lcm-op-occ job)))) + + +;; +;; Operation History Queries +;; + +(defn query-operations + "Query operation occurrences with filtering. + + Parameters: + - filters: Map of filter criteria + * :app-instance-id - Filter by application instance + * :operation-type - Filter by operation type (INSTANTIATE, TERMINATE, OPERATE) + * :operation-state - Filter by operation state (PROCESSING, COMPLETED, FAILED) + * :start-time-after - Filter operations started after this time + * :start-time-before - Filter operations started before this time + - options: Query options + * :limit - Maximum number of results (default: 100) + * :offset - Offset for pagination (default: 0) + * :sort-by - Field to sort by (default: :start-time) + * :sort-order - Sort order :asc or :desc (default: :desc) + + Returns: + - Vector of AppLcmOpOcc maps" + [filters options] + (log/info "Querying operations with filters:" filters) + (let [limit (get options :limit 100) + offset (get options :offset 0) + sort-by (get options :sort-by :start-time) + sort-order (get options :sort-order :desc)] + + ;; In a real implementation, this would query the job collection + ;; with the specified filters and return AppLcmOpOcc representations + ;; For now, return empty vector + [])) + + +(defn get-operation-history + "Get operation history for a specific application instance. + + Parameters: + - app-instance-id: ID of the application instance + - options: Query options (same as query-operations) + + Returns: + - Vector of AppLcmOpOcc maps in reverse chronological order" + [app-instance-id options] + (log/info "Getting operation history for" app-instance-id) + (query-operations {:app-instance-id app-instance-id} + (merge {:sort-order :desc} options))) + + +(defn get-operation-by-id + "Get a specific operation occurrence by ID. + + Parameters: + - op-occ-id: ID of the operation occurrence (job ID) + + Returns: + - AppLcmOpOcc map or nil if not found" + [op-occ-id] + (log/info "Getting operation occurrence" op-occ-id) + (get-operation-state op-occ-id)) + + +;; +;; Operation Statistics +;; + +(defn get-operation-stats + "Get statistics about operations for an application instance. + + Parameters: + - app-instance-id: ID of the application instance + + Returns: + - Map with operation statistics: + * :total - Total number of operations + * :by-type - Count by operation type + * :by-state - Count by operation state + * :success-rate - Percentage of successful operations + * :avg-duration - Average operation duration in seconds" + [app-instance-id] + (log/info "Getting operation statistics for" app-instance-id) + (let [operations (get-operation-history app-instance-id {}) + total (count operations) + by-type (frequencies (map :operationType operations)) + by-state (frequencies (map :operationState operations)) + completed (filter #(= "COMPLETED" (:operationState %)) operations) + success-rate (if (pos? total) + (* 100.0 (/ (count completed) total)) + 0.0)] + {:total total + :by-type by-type + :by-state by-state + :success-rate success-rate + :avg-duration 0.0})) + + +;; +;; Integration Helpers +;; + +(defn wrap-with-job-tracking + "Wrap a lifecycle operation function with job tracking. + + This higher-order function creates a job before executing the operation, + updates the job state during execution, and marks it complete/failed after. + + Parameters: + - operation-fn: Function to execute (takes request-params, returns result map) + - operation-type: Type of operation (:INSTANTIATE, :TERMINATE, :OPERATE) + - app-instance-id: ID of the application instance + - request-params: Operation-specific parameters + - user-id: ID of the user initiating the operation + + Returns: + - Map with :job-id and :operation-result" + [operation-fn operation-type app-instance-id request-params user-id] + (let [job (create-operation-job operation-type app-instance-id request-params user-id) + job-id (:id job)] + (try + ;; Create job in database (would be a real DB operation) + (log/info "Created job" job-id "for operation" operation-type) + + ;; Start the job + (start-operation-job job-id) + + ;; Execute the operation + (let [result (operation-fn request-params)] + (if (= :SUCCESS (:status result)) + ;; Operation succeeded + (do + (complete-operation-job job-id result) + {:job-id job-id + :operation-result result + :app-lcm-op-occ (job->app-lcm-op-occ + (merge job + (complete-operation-job job-id result)))}) + ;; Operation failed + (let [error-detail (:error-detail result)] + (fail-operation-job job-id error-detail) + {:job-id job-id + :operation-result result + :app-lcm-op-occ (job->app-lcm-op-occ + (merge job + (fail-operation-job job-id error-detail)))}))) + + (catch Exception e + (log/error e "Exception during operation execution") + (let [error-detail {:type "about:blank" + :title "Operation Execution Error" + :status 500 + :detail (.getMessage e)}] + (fail-operation-job job-id error-detail) + {:job-id job-id + :operation-result {:status :FAILED :error-detail error-detail} + :app-lcm-op-occ (job->app-lcm-op-occ + (merge job + (fail-operation-job job-id error-detail)))}))))) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_subscription.clj b/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_subscription.clj new file mode 100644 index 000000000..f69c86f2a --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_subscription.clj @@ -0,0 +1,356 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-lcm-subscription + "MEC 010-2 Application Lifecycle Subscription + + Implements ETSI GS MEC 010-2 v2.2.1 subscription model for notifications: + - AppInstanceStateChangeNotification: App instance state changes + - AppLcmOpOccStateChangeNotification: Operation occurrence state changes + + Subscriptions allow MEO consumers to receive asynchronous notifications + about lifecycle events via HTTP callbacks." + (:require + [clojure.spec.alpha :as s] + [clojure.tools.logging :as log])) + + +;; +;; Subscription Types +;; + +(def subscription-types + "Valid MEC 010-2 subscription types" + #{"AppInstanceStateChangeNotification" + "AppLcmOpOccStateChangeNotification"}) + + +;; +;; Notification Types (aligned with subscription types) +;; + +(def notification-types + "Valid MEC 010-2 notification types" + #{"AppInstanceStateChangeNotification" + "AppLcmOpOccStateChangeNotification"}) + + +;; +;; Change Types +;; + +(def app-instance-change-types + "Change types for AppInstance notifications" + #{"INSTANTIATION_STATE" ; instantiationState changed + "OPERATIONAL_STATE" ; operationalState changed + "CONFIGURATION"}) ; Configuration changed + +(def op-occ-change-types + "Change types for AppLcmOpOcc notifications" + #{"OPERATION_STATE" ; operationState changed + "OPERATION_RESULT"}) ; Operation completed with result + + +;; +;; Schema Definitions +;; + +(s/def ::id string?) +(s/def ::subscription-type subscription-types) +(s/def ::callback-uri (s/and string? #(re-matches #"https?://.*" %))) + +;; Filter for AppInstance notifications +(s/def ::app-instance-id (s/nilable string?)) +(s/def ::app-name (s/nilable string?)) +(s/def ::operational-state (s/nilable #{"STARTED" "STOPPED" "UNKNOWN"})) +(s/def ::instantiation-state (s/nilable #{"NOT_INSTANTIATED" "INSTANTIATED"})) + +(s/def ::app-instance-filter + (s/keys :opt-un [::app-instance-id + ::app-name + ::operational-state + ::instantiation-state])) + +;; Filter for AppLcmOpOcc notifications +(s/def ::operation-type (s/nilable #{"INSTANTIATE" "TERMINATE" "OPERATE"})) +(s/def ::operation-state (s/nilable #{"STARTING" "PROCESSING" "COMPLETED" "FAILED" "ROLLED_BACK"})) + +(s/def ::app-lcm-op-occ-filter + (s/keys :opt-un [::app-instance-id + ::operation-type + ::operation-state])) + +;; Subscription resource +(s/def ::subscription + (s/keys :req-un [::id + ::subscription-type + ::callback-uri] + :opt-un [::app-instance-filter + ::app-lcm-op-occ-filter + ::created + ::updated + ::owner])) + + +;; +;; Subscription Resource Functions +;; + +(defn create-subscription + "Create a new subscription resource. + + Parameters: + - subscription-type: One of subscription-types + - callback-uri: HTTP(S) URI for webhook callbacks + - filter-opts: Optional filter criteria (map) + - user-id: Owner of the subscription + + Returns: + Subscription resource map with generated ID" + [subscription-type callback-uri filter-opts user-id] + (let [subscription-id (str "subscription/" (java.util.UUID/randomUUID)) + now (java.time.Instant/now) + filter-key (case subscription-type + "AppInstanceStateChangeNotification" + :app-instance-filter + + "AppLcmOpOccStateChangeNotification" + :app-lcm-op-occ-filter + + nil)] + (cond-> {:id subscription-id + :subscription-type subscription-type + :callback-uri callback-uri + :created now + :updated now + :owner user-id + :active true} + + (and filter-key (seq filter-opts)) + (assoc filter-key filter-opts)))) + + +(defn validate-subscription + "Validate subscription resource against spec. + + Returns: + - {:valid? true} if valid + - {:valid? false :errors [...]} if invalid" + [subscription] + (if (s/valid? ::subscription subscription) + {:valid? true} + {:valid? false + :errors (s/explain-data ::subscription subscription)})) + + +(defn update-subscription + "Update subscription resource fields. + + Allowed updates: + - callback-uri + - filter (app-instance-filter or app-lcm-op-occ-filter) + - active (boolean) + + Returns: + Updated subscription map" + [subscription updates] + (let [now (java.time.Instant/now) + allowed-updates (select-keys updates [:callback-uri + :app-instance-filter + :app-lcm-op-occ-filter + :active])] + (-> subscription + (merge allowed-updates) + (assoc :updated now)))) + + +(defn deactivate-subscription + "Mark subscription as inactive (soft delete). + + Returns: + Updated subscription map with :active false" + [subscription] + (assoc subscription + :active false + :updated (java.time.Instant/now))) + + +;; +;; Filter Matching +;; + +(defn- matches-filter? + "Check if a value matches filter criteria. + + Filter criteria: + - nil: matches anything (no filter) + - value: must equal value + - collection: must be in collection" + [filter-value actual-value] + (cond + (nil? filter-value) + true + + (coll? filter-value) + (contains? (set filter-value) actual-value) + + :else + (= filter-value actual-value))) + + +(defn matches-app-instance-filter? + "Check if app instance matches subscription filter. + + Parameters: + - subscription: Subscription resource + - app-instance: AppInstance resource + + Returns: + Boolean indicating if app instance matches filter" + [subscription app-instance] + (let [filter (:app-instance-filter subscription)] + (if (empty? filter) + true ; No filter = match all + (and + (matches-filter? (:app-instance-id filter) (:id app-instance)) + (matches-filter? (:app-name filter) (:app-name app-instance)) + (matches-filter? (:operational-state filter) (:operational-state app-instance)) + (matches-filter? (:instantiation-state filter) (:instantiation-state app-instance)))))) + + +(defn matches-app-lcm-op-occ-filter? + "Check if operation occurrence matches subscription filter. + + Parameters: + - subscription: Subscription resource + - app-lcm-op-occ: AppLcmOpOcc resource + + Returns: + Boolean indicating if operation matches filter" + [subscription app-lcm-op-occ] + (let [filter (:app-lcm-op-occ-filter subscription)] + (if (empty? filter) + true ; No filter = match all + (and + (matches-filter? (:app-instance-id filter) (:app-instance-id app-lcm-op-occ)) + (matches-filter? (:operation-type filter) (:operation-type app-lcm-op-occ)) + (matches-filter? (:operation-state filter) (:operation-state app-lcm-op-occ)))))) + + +;; +;; Notification Building +;; + +(defn build-app-instance-notification + "Build AppInstanceStateChangeNotification. + + Parameters: + - subscription: Subscription resource + - app-instance: AppInstance resource + - change-type: Type of change (from app-instance-change-types) + - previous-state: Previous state before change (optional) + + Returns: + Notification map ready for delivery" + [subscription app-instance change-type previous-state] + {:notification-type "AppInstanceStateChangeNotification" + :notification-id (str "notification/" (java.util.UUID/randomUUID)) + :subscription-id (:id subscription) + :timestamp (java.time.Instant/now) + :app-instance-id (:id app-instance) + :app-name (:app-name app-instance) + :app-d-id (:app-d-id app-instance) + :instantiation-state (:instantiation-state app-instance) + :operational-state (:operational-state app-instance) + :change-type change-type + :previous-state previous-state + :_links {:subscription {:href (str "/mec/app_lcm/v2/subscriptions/" (:id subscription))} + :app-instance {:href (str "/mec/app_lcm/v2/app_instances/" (:id app-instance))}}}) + + +(defn build-app-lcm-op-occ-notification + "Build AppLcmOpOccStateChangeNotification. + + Parameters: + - subscription: Subscription resource + - app-lcm-op-occ: AppLcmOpOcc resource + - change-type: Type of change (from op-occ-change-types) + - previous-state: Previous state before change (optional) + + Returns: + Notification map ready for delivery" + [subscription app-lcm-op-occ change-type previous-state] + {:notification-type "AppLcmOpOccStateChangeNotification" + :notification-id (str "notification/" (java.util.UUID/randomUUID)) + :subscription-id (:id subscription) + :timestamp (java.time.Instant/now) + :app-lcm-op-occ-id (:id app-lcm-op-occ) + :app-instance-id (:app-instance-id app-lcm-op-occ) + :operation-type (:operation-type app-lcm-op-occ) + :operation-state (:operation-state app-lcm-op-occ) + :change-type change-type + :previous-state previous-state + :start-time (:start-time app-lcm-op-occ) + :state-entered-time (:state-entered-time app-lcm-op-occ) + :_links {:subscription {:href (str "/mec/app_lcm/v2/subscriptions/" (:id subscription))} + :app-lcm-op-occ {:href (str "/mec/app_lcm/v2/app_lcm_op_occs/" (:id app-lcm-op-occ))} + :app-instance {:href (str "/mec/app_lcm/v2/app_instances/" (:app-instance-id app-lcm-op-occ))}}}) + + +;; +;; Query Functions +;; + +(defn query-subscriptions + "Query subscriptions with optional filters. + + Parameters: + - subscriptions: Collection of subscription resources + - opts: Query options + * :subscription-type - Filter by subscription type + * :owner - Filter by owner + * :active - Filter by active status (default true) + * :limit - Maximum results (default 100) + * :offset - Skip first N results (default 0) + + Returns: + Filtered and paginated collection of subscriptions" + [subscriptions {:keys [subscription-type owner active limit offset] + :or {active true limit 100 offset 0}}] + (->> subscriptions + (filter (fn [sub] + (and + (or (nil? subscription-type) + (= (:subscription-type sub) subscription-type)) + (or (nil? owner) + (= (:owner sub) owner)) + (or (nil? active) + (= (:active sub) active))))) + (drop offset) + (take limit))) + + +(defn get-subscription-by-id + "Get subscription by ID. + + Parameters: + - subscriptions: Collection of subscription resources + - subscription-id: Subscription ID + + Returns: + Subscription resource or nil if not found" + [subscriptions subscription-id] + (first (filter #(= (:id %) subscription-id) subscriptions))) + + +(defn get-active-subscriptions-for-type + "Get all active subscriptions for a specific notification type. + + Parameters: + - subscriptions: Collection of subscription resources + - subscription-type: Subscription type to filter + + Returns: + Collection of active subscriptions" + [subscriptions subscription-type] + (filter (fn [sub] + (and (:active sub) + (= (:subscription-type sub) subscription-type))) + subscriptions)) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_v2.clj b/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_v2.clj new file mode 100644 index 000000000..1ef83dbb0 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/app_lcm_v2.clj @@ -0,0 +1,517 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-lcm-v2 + "MEC 010-2 Application Lifecycle Management API v2 + + Provides RESTful endpoints compliant with ETSI GS MEC 010-2 v2.2.1 + for application lifecycle management at the MEO level. + + Endpoints: + - POST /app_lcm/v2/app_instances - Create app instance + - GET /app_lcm/v2/app_instances - List app instances + - GET /app_lcm/v2/app_instances/{id} - Get app instance + - DELETE /app_lcm/v2/app_instances/{id} - Delete app instance + - POST /app_lcm/v2/app_instances/{id}/instantiate - Instantiate + - POST /app_lcm/v2/app_instances/{id}/terminate - Terminate + - POST /app_lcm/v2/app_instances/{id}/operate - Start/Stop + - GET /app_lcm/v2/app_lcm_op_occs - List operations + - GET /app_lcm/v2/app_lcm_op_occs/{id} - Get operation + - POST /app_lcm/v2/subscriptions - Create subscription + - GET /app_lcm/v2/subscriptions - List subscriptions + - GET /app_lcm/v2/subscriptions/{id} - Get subscription + - DELETE /app_lcm/v2/subscriptions/{id} - Delete subscription + + Standard: ETSI GS MEC 010-2 v2.2.1" + (:require + [clojure.tools.logging :as log] + [com.sixsq.nuvla.server.resources.common.crud :as crud] + [com.sixsq.nuvla.server.resources.common.std-crud :as std-crud] + [com.sixsq.nuvla.server.resources.common.utils :as u] + [com.sixsq.nuvla.server.resources.mec.app-instance :as app-instance] + [com.sixsq.nuvla.server.resources.mec.app-lcm-op-occ :as app-lcm-op-occ] + [com.sixsq.nuvla.server.resources.mec.app-lcm-subscription :as subscription] + [com.sixsq.nuvla.server.resources.mec.lifecycle-handler :as lifecycle] + [com.sixsq.nuvla.server.resources.mec.query-filter :as qf] + [com.sixsq.nuvla.server.util.response :as r])) + + +;; +;; Resource Type Definition +;; + +(def ^:const resource-type "mec-app-lcm") +(def ^:const collection-type "mec-app-lcm-collection") +(def ^:const api-version "v2") +(def ^:const base-uri (str "app_lcm/" api-version)) + + +;; +;; Error Handling (RFC 7807 ProblemDetails) +;; + +(defn problem-details + "Creates an RFC 7807 ProblemDetails error response" + [type title status & {:keys [detail instance]}] + {:type (or type "about:blank") + :title title + :status status + :detail (or detail title) + :instance instance}) + + +(defn not-found-error + "Returns a 404 Not Found error in ProblemDetails format" + [resource-id] + (problem-details + "https://docs.nuvla.io/mec/errors/not-found" + "Resource Not Found" + 404 + :detail (str "App instance " resource-id " not found") + :instance resource-id)) + + +(defn validation-error + "Returns a 400 Bad Request error in ProblemDetails format" + [detail] + (problem-details + "https://docs.nuvla.io/mec/errors/validation" + "Validation Error" + 400 + :detail detail)) + + +(defn conflict-error + "Returns a 409 Conflict error in ProblemDetails format" + [detail] + (problem-details + "https://docs.nuvla.io/mec/errors/conflict" + "Resource Conflict" + 409 + :detail detail)) + + +;; +;; App Instance Endpoints +;; + +(defn create-app-instance-handler + "POST /app_lcm/v2/app_instances - Create a new app instance" + [request] + (try + (let [body (:body request) + _ (app-instance/validate-app-instance-info body)] + ;; TODO: Integrate with deployment CRUD create + (r/json-response {:message "App instance creation pending integration" + :appInstanceInfo body} 501)) + (catch Exception e + (log/error e "Failed to create app instance") + (r/json-response (validation-error (ex-message e)) 400)))) + + +(defn list-app-instances-handler + "GET /app_lcm/v2/app_instances - List all app instances + + Supports MEC 010-2 query parameters: + - filter: FIQL-like filter expression (e.g., (eq,appName,my-app)) + - page: Page number (1-based, default 1) + - size: Page size (default 20, max 100) + - fields: Comma-separated field names for field selection" + [request] + (try + (let [params (:params request) + ;; TODO: Replace with actual deployment CRUD query + ;; For now, return empty collection with query processing + resources []] + (r/json-response (qf/process-query resources + (merge params + {:base-uri (str "/" base-uri "/app_instances")})))) + (catch Exception e + (log/error e "Failed to list app instances") + (r/json-response (validation-error (ex-message e)) 400)))) + + +(defn get-app-instance-handler + "GET /app_lcm/v2/app_instances/{id} - Get a specific app instance" + [request] + (let [app-instance-id (get-in request [:params :id])] + ;; TODO: Integrate with deployment CRUD read + (r/json-response (not-found-error app-instance-id) 404))) + + +(defn delete-app-instance-handler + "DELETE /app_lcm/v2/app_instances/{id} - Delete an app instance" + [request] + (let [app-instance-id (get-in request [:params :id])] + (try + ;; TODO: Integrate with deployment CRUD delete + (r/json-response {:message "App instance deletion pending integration"} 501) + (catch Exception e + (log/error e "Failed to delete app instance" app-instance-id) + (r/json-response (not-found-error app-instance-id) 404))))) + + +;; +;; Lifecycle Operation Endpoints +;; + +(defn instantiate-app-instance-handler + "POST /app_lcm/v2/app_instances/{id}/instantiate - Instantiate an app" + [request] + (let [app-instance-id (get-in request [:params :id]) + body (:body request)] + (try + (log/info "Instantiate request for" app-instance-id) + + ;; Execute instantiation via lifecycle handler + (let [op-occ (lifecycle/instantiate app-instance-id body)] + (log/info "Instantiation operation created:" (:lcmOpOccId op-occ)) + + ;; Return operation occurrence with 202 Accepted + (r/json-response op-occ 202)) + + (catch clojure.lang.ExceptionInfo e + (log/error e "Failed to instantiate app instance" app-instance-id) + (r/json-response (validation-error (ex-message e)) 400)) + (catch Exception e + (log/error e "Unexpected error during instantiation" app-instance-id) + (r/json-response (problem-details + "about:blank" + "Internal Server Error" + 500 + :detail (ex-message e) + :instance app-instance-id) 500))))) + + +(defn terminate-app-instance-handler + "POST /app_lcm/v2/app_instances/{id}/terminate - Terminate an app" + [request] + (let [app-instance-id (get-in request [:params :id]) + body (:body request)] + (try + (log/info "Terminate request for" app-instance-id) + + ;; Execute termination via lifecycle handler + (let [op-occ (lifecycle/terminate app-instance-id body)] + (log/info "Termination operation created:" (:lcmOpOccId op-occ)) + + ;; Return operation occurrence with 202 Accepted + (r/json-response op-occ 202)) + + (catch clojure.lang.ExceptionInfo e + (log/error e "Failed to terminate app instance" app-instance-id) + (r/json-response (validation-error (ex-message e)) 400)) + (catch Exception e + (log/error e "Unexpected error during termination" app-instance-id) + (r/json-response (problem-details + "about:blank" + "Internal Server Error" + 500 + :detail (ex-message e) + :instance app-instance-id) 500))))) + + +(defn operate-app-instance-handler + "POST /app_lcm/v2/app_instances/{id}/operate - Start/Stop an app" + [request] + (let [app-instance-id (get-in request [:params :id]) + body (:body request)] + (try + (log/info "Operate request for" app-instance-id "changeStateTo" (:changeStateTo body)) + + ;; Execute operate via lifecycle handler + (let [op-occ (lifecycle/operate app-instance-id body)] + (log/info "Operate operation created:" (:lcmOpOccId op-occ)) + + ;; Return operation occurrence with 202 Accepted + (r/json-response op-occ 202)) + + (catch clojure.lang.ExceptionInfo e + (log/error e "Failed to operate app instance" app-instance-id) + (r/json-response (validation-error (ex-message e)) 400)) + (catch Exception e + (log/error e "Unexpected error during operate" app-instance-id) + (r/json-response (problem-details + "about:blank" + "Internal Server Error" + 500 + :detail (ex-message e) + :instance app-instance-id) 500))))) + + +;; +;; Operation Occurrence Endpoints +;; + +(defn list-app-lcm-op-occs-handler + "GET /app_lcm/v2/app_lcm_op_occs - List all operation occurrences + + Supports MEC 010-2 query parameters: + - filter: FIQL-like filter expression (e.g., (eq,operationType,INSTANTIATE)) + - page: Page number (1-based, default 1) + - size: Page size (default 20, max 100) + - fields: Comma-separated field names for field selection" + [request] + (try + (let [params (:params request) + ;; TODO: Replace with actual job CRUD query + ;; For now, return empty collection with query processing + resources []] + (r/json-response (qf/process-query resources + (merge params + {:base-uri (str "/" base-uri "/app_lcm_op_occs")})))) + (catch Exception e + (log/error e "Failed to list operation occurrences") + (r/json-response (validation-error (ex-message e)) 400)))) + + +(defn get-app-lcm-op-occ-handler + "GET /app_lcm/v2/app_lcm_op_occs/{id} - Get a specific operation occurrence" + [request] + (let [lcm-op-occ-id (get-in request [:params :id])] + ;; TODO: Integrate with job CRUD read + (r/json-response (not-found-error lcm-op-occ-id) 404))) + + +;; +;; Subscription Endpoints +;; + +;; In-memory subscription store (TODO: Replace with persistent storage) +(def subscription-store (atom [])) + +(defn create-subscription-handler + "POST /app_lcm/v2/subscriptions - Create a new subscription" + [request] + (try + (let [body (:body request) + subscription-type (:subscriptionType body) + callback-uri (:callbackUri body) + filter-opts (or (:appInstanceFilter body) + (:appLcmOpOccFilter body) + {}) + user-id (or (get-in request [:identity :user-id]) + "user/anonymous")] + + ;; Validate subscription type + (when-not (contains? subscription/subscription-types subscription-type) + (throw (ex-info "Invalid subscription type" + {:status 400 + :subscription-type subscription-type}))) + + ;; Create subscription + (let [sub (subscription/create-subscription + subscription-type + callback-uri + filter-opts + user-id)] + + ;; Validate subscription + (let [validation (subscription/validate-subscription sub)] + (when-not (:valid? validation) + (throw (ex-info "Invalid subscription" + {:status 400 + :errors (:errors validation)})))) + + ;; Store subscription + (swap! subscription-store conj sub) + + (log/info "Created subscription" (:id sub) "for user" user-id) + (r/json-response sub 201))) + + (catch clojure.lang.ExceptionInfo e + (let [data (ex-data e)] + (log/error e "Failed to create subscription") + (r/json-response (validation-error (ex-message e)) + (or (:status data) 400)))) + (catch Exception e + (log/error e "Unexpected error creating subscription") + (r/json-response (problem-details + "about:blank" + "Internal Server Error" + 500 + :detail (ex-message e)) 500)))) + + +(defn list-subscriptions-handler + "GET /app_lcm/v2/subscriptions - List subscriptions + + Supports MEC 010-2 query parameters: + - filter: FIQL-like filter expression (e.g., (eq,subscriptionType,AppInstanceStateChangeNotification)) + - page: Page number (1-based, default 1) + - size: Page size (default 20, max 100) + - fields: Comma-separated field names for field selection + + Also supports legacy parameters for backward compatibility: + - subscriptionType: Filter by subscription type (deprecated, use filter parameter instead) + - limit/offset: Pagination (deprecated, use page/size instead)" + [request] + (try + (let [query-params (:params request) + user-id (get-in request [:identity :user-id]) + + ;; Filter subscriptions by user and active status + active-user-subs (filter (fn [s] + (and (:active s) + (or (nil? user-id) + (= (:owner s) user-id)))) + @subscription-store)] + + ;; Apply query processing (filter, pagination, field selection) + (r/json-response (qf/process-query (vec active-user-subs) + (merge query-params + {:base-uri (str "/" base-uri "/subscriptions")})))) + + (catch Exception e + (log/error e "Failed to list subscriptions") + (r/json-response (problem-details + "about:blank" + "Internal Server Error" + 500 + :detail (ex-message e)) 500)))) + + +(defn get-subscription-handler + "GET /app_lcm/v2/subscriptions/{id} - Get a specific subscription" + [request] + (try + (let [subscription-id (get-in request [:params :id]) + user-id (get-in request [:identity :user-id]) + sub (subscription/get-subscription-by-id + @subscription-store + subscription-id)] + + (cond + (nil? sub) + (r/json-response (not-found-error subscription-id) 404) + + (and user-id (not= (:owner sub) user-id)) + (r/json-response (problem-details + "https://docs.nuvla.io/mec/errors/forbidden" + "Access Forbidden" + 403 + :detail "You do not have permission to access this subscription" + :instance subscription-id) 403) + + :else + (r/json-response sub))) + + (catch Exception e + (log/error e "Failed to get subscription") + (r/json-response (problem-details + "about:blank" + "Internal Server Error" + 500 + :detail (ex-message e)) 500)))) + + +(defn delete-subscription-handler + "DELETE /app_lcm/v2/subscriptions/{id} - Delete a subscription" + [request] + (try + (let [subscription-id (get-in request [:params :id]) + user-id (get-in request [:identity :user-id]) + sub (subscription/get-subscription-by-id + @subscription-store + subscription-id)] + + (cond + (nil? sub) + (r/json-response (not-found-error subscription-id) 404) + + (and user-id (not= (:owner sub) user-id)) + (r/json-response (problem-details + "https://docs.nuvla.io/mec/errors/forbidden" + "Access Forbidden" + 403 + :detail "You do not have permission to delete this subscription" + :instance subscription-id) 403) + + :else + (do + ;; Deactivate subscription (soft delete) + (swap! subscription-store + (fn [subs] + (mapv (fn [s] + (if (= (:id s) subscription-id) + (subscription/deactivate-subscription s) + s)) + subs))) + + (log/info "Deleted subscription" subscription-id) + (r/json-response nil 204)))) + + (catch Exception e + (log/error e "Failed to delete subscription") + (r/json-response (problem-details + "about:blank" + "Internal Server Error" + 500 + :detail (ex-message e)) 500)))) + + +;; +;; Route Definitions +;; + +(def routes + "MEC 010-2 API routes" + [;; App Instance Collection + [(str "/" base-uri "/app_instances") + {:get {:handler list-app-instances-handler + :summary "List app instances"} + :post {:handler create-app-instance-handler + :summary "Create app instance"}}] + + ;; App Instance Item + [(str "/" base-uri "/app_instances/:id") + {:get {:handler get-app-instance-handler + :summary "Get app instance"} + :delete {:handler delete-app-instance-handler + :summary "Delete app instance"}}] + + ;; App Instance Lifecycle Operations + [(str "/" base-uri "/app_instances/:id/instantiate") + {:post {:handler instantiate-app-instance-handler + :summary "Instantiate app instance"}}] + + [(str "/" base-uri "/app_instances/:id/terminate") + {:post {:handler terminate-app-instance-handler + :summary "Terminate app instance"}}] + + [(str "/" base-uri "/app_instances/:id/operate") + {:post {:handler operate-app-instance-handler + :summary "Operate app instance (start/stop)"}}] + + ;; Operation Occurrence Collection + [(str "/" base-uri "/app_lcm_op_occs") + {:get {:handler list-app-lcm-op-occs-handler + :summary "List operation occurrences"}}] + + ;; Operation Occurrence Item + [(str "/" base-uri "/app_lcm_op_occs/:id") + {:get {:handler get-app-lcm-op-occ-handler + :summary "Get operation occurrence"}}] + + ;; Subscription Collection + [(str "/" base-uri "/subscriptions") + {:get {:handler list-subscriptions-handler + :summary "List subscriptions"} + :post {:handler create-subscription-handler + :summary "Create subscription"}}] + + ;; Subscription Item + [(str "/" base-uri "/subscriptions/:id") + {:get {:handler get-subscription-handler + :summary "Get subscription"} + :delete {:handler delete-subscription-handler + :summary "Delete subscription"}}]]) + + +;; +;; Initialization +;; + +(defn initialize + "Initialize MEC 010-2 API" + [] + (log/info "Initializing MEC 010-2 Application Lifecycle Management API") + (log/info "API version:" api-version) + (log/info "Base URI:" base-uri)) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/app_package.clj b/code/src/com/sixsq/nuvla/server/resources/mec/app_package.clj new file mode 100644 index 000000000..8a82e25a6 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/app_package.clj @@ -0,0 +1,372 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-package + "ETSI MEC 010-2 Mm1 Application Package Management API + + Reference Point: Mm1 (OSS ↔ MEO) + Standard: ETSI GS MEC 010-2 v2.2.1 + Sections: 7.3.1, 7.3.2 + + The Mm1 reference point enables OSS to: + - Query available application packages (GET /app_packages) + - Get specific application package details (GET /app_packages/{appPkgId}) + - Onboard new application packages (POST /app_packages) + - Delete application packages (DELETE /app_packages/{appPkgId}) + + This implementation integrates with Nuvla's module catalog, + treating modules as application packages." + (:require + [clojure.tools.logging :as log] + [com.sixsq.nuvla.auth.acl-resource :as a] + [com.sixsq.nuvla.auth.utils :as auth] + [com.sixsq.nuvla.db.filter.parser :as parser] + [com.sixsq.nuvla.server.resources.common.crud :as crud] + [com.sixsq.nuvla.server.resources.common.std-crud :as std-crud] + [com.sixsq.nuvla.server.resources.common.utils :as u] + [com.sixsq.nuvla.server.resources.spec.module-application-mec :as mec-spec] + [com.sixsq.nuvla.server.util.response :as r] + [ring.util.response :as rur])) + + +(def ^:const resource-type "mec-app-package") + + +;; +;; Utility functions to map Nuvla modules to MEC AppPkgInfo +;; + +(defn module->app-pkg-info + "Converts a Nuvla module to MEC AppPkgInfo format (ETSI MEC 010-2 section 7.3.2.2)" + [module] + (let [content (:content module) + subtype (:subtype module)] + {:id (:id module) + :appPkgId (:id module) + :appDId (or (:appDId content) (:id module)) + :appName (or (:appName content) (:name module)) + :appProvider (or (:appProvider content) (:parent-path module)) + :appSoftVersion (or (:appSoftVersion content) + (str (count (:versions module)))) + :appDVersion (or (:appDVersion content) "1.0") + :checksum {:algorithm "SHA-256" + :hash (or (:content-id module) "not-computed")} + :operationalState "ENABLED" + :usageState (if (:published module) "IN_USE" "NOT_IN_USE") + :onboardingState "ONBOARDED" + :appPkgPath (:path module) + :moduletype (:subtype module) + :created (:created module) + :updated (:updated module)})) + + +(defn app-pkg-filter + "Creates a filter to query MEC-capable modules (application_mec subtype)" + [additional-filter] + (let [base-filter "subtype='application_mec'" + filter-str (if additional-filter + (str base-filter " and " additional-filter) + base-filter)] + filter-str)) + + +;; +;; GET /app_packages - Query application packages +;; ETSI MEC 010-2 section 7.3.1.3.1 +;; + +(defn query-app-packages + "Query application packages with optional filters + + Query parameters (per ETSI MEC 010-2): + - appPkgId: Application package identifier + - appDId: Application descriptor identifier + - appName: Application name + - appProvider: Application provider + - appSoftVersion: Application software version + - operationalState: Operational state (ENABLED/DISABLED) + - usageState: Usage state (IN_USE/NOT_IN_USE) + - onboardingState: Onboarding state (CREATED/UPLOADING/PROCESSING/ONBOARDED) + + Returns: AppPkgInfo[] (section 7.3.2.2)" + [request] + (try + (let [params (:params request) + + ;; Extract MEC query parameters + app-pkg-id (:appPkgId params) + app-d-id (:appDId params) + app-name (:appName params) + app-provider (:appProvider params) + app-soft-version (:appSoftVersion params) + operational-state (:operationalState params) + usage-state (:usageState params) + onboarding-state (:onboardingState params) + + ;; Build Nuvla filter from MEC parameters + filters (cond-> [] + app-pkg-id (conj (str "id='" app-pkg-id "'")) + app-d-id (conj (str "content/appDId='" app-d-id "'")) + app-name (conj (str "content/appName='" app-name "' or name='" app-name "'")) + app-provider (conj (str "content/appProvider='" app-provider "'")) + app-soft-version (conj (str "content/appSoftVersion='" app-soft-version "'")) + (= usage-state "IN_USE") (conj "published=true") + (= usage-state "NOT_IN_USE") (conj "published=false")) + + filter-str (app-pkg-filter (when (seq filters) + (clojure.string/join " and " filters))) + + ;; Query modules + modules (crud/query-as-admin "module" {:cimi-params {:filter (parser/parse-cimi-filter filter-str) + :orderby [["created" :desc]]}}) + + ;; Convert to AppPkgInfo format + app-packages (map module->app-pkg-info (:resources modules))] + + (log/info "Mm1: Query app_packages, found" (count app-packages) "packages" + "filters:" filter-str) + + (r/json-response {:AppPkgInfo app-packages + :_links {:self {:href "/mec/app_lcm/v2/app_packages"}}})) + + (catch Exception e + (log/error e "Mm1: Error querying app_packages") + (r/json-response {:type "about:blank" + :title "Internal Server Error" + :status 500 + :detail (.getMessage e)} + 500)))) + + +;; +;; GET /app_packages/{appPkgId} - Get specific application package +;; ETSI MEC 010-2 section 7.3.1.3.2 +;; + +(defn get-app-package + "Get information about a specific application package + + Path parameter: + - appPkgId: Application package identifier (module ID) + + Returns: AppPkgInfo (section 7.3.2.2)" + [request] + (try + (let [app-pkg-id (get-in request [:params :appPkgId]) + + ;; Retrieve module + module (crud/retrieve-by-id-as-admin app-pkg-id) + + ;; Check if module exists + _ (when-not module + (throw (ex-info "Application package not found" + {:status 404 + :type "https://forge.etsi.org/rep/mec/gs010-2-app-pkg-lcm-api/problems/not-found" + :title "Not Found" + :detail (str "Application package " app-pkg-id " not found")}))) + + ;; Convert to AppPkgInfo + app-pkg-info (module->app-pkg-info module)] + + (log/info "Mm1: Get app_package" app-pkg-id) + + (r/json-response app-pkg-info)) + + (catch clojure.lang.ExceptionInfo e + (let [data (ex-data e)] + (log/warn "Mm1: App package not found:" (get-in request [:params :appPkgId])) + (r/json-response {:type (or (:type data) "about:blank") + :title (or (:title data) "Not Found") + :status (or (:status data) 404) + :detail (or (:detail data) (.getMessage e))} + (or (:status data) 404)))) + + (catch Exception e + (log/error e "Mm1: Error getting app_package") + (r/json-response {:type "about:blank" + :title "Internal Server Error" + :status 500 + :detail (.getMessage e)} + 500)))) + + +;; +;; POST /app_packages - Onboard application package +;; ETSI MEC 010-2 section 7.3.1.3.3 +;; + +(defn create-app-package + "Onboard a new application package + + Request body: CreateAppPkg (section 7.3.2.3) + - appPkgName: Name of the application package + - appPkgVersion: Version of the application package + - appPkgPath: Optional path where package is stored + - userDefinedData: Optional key-value pairs for user metadata + + Returns: AppPkgInfo (section 7.3.2.2)" + [request] + (try + (let [body (:body request) + app-pkg-name (:appPkgName body) + app-pkg-version (:appPkgVersion body) + app-pkg-path (:appPkgPath body) + user-defined-data (:userDefinedData body) + + ;; Validate required fields + _ (when-not app-pkg-name + (throw (ex-info "appPkgName is required" + {:status 400 + :type "https://forge.etsi.org/rep/mec/gs010-2-app-pkg-lcm-api/problems/bad-request" + :title "Bad Request" + :detail "appPkgName is required"}))) + + ;; Create minimal MEC AppD module + ;; This creates a placeholder module that can be updated with full AppD content later + user-id (or (get-in request [:identity :user]) "internal") + + module-request {:params {:resource-name "module"} + :body {:name app-pkg-name + :description (str "MEC Application Package: " app-pkg-name) + :subtype "application_mec" + :path (or app-pkg-path (str "mec-apps/" app-pkg-name)) + :parent-path (str "mec-apps/" (or (:appProvider body) user-id)) + :versions [{:href (str app-pkg-name "/" (or app-pkg-version "1.0.0"))}] + :published false + :content {:appName app-pkg-name + :appDId (str "appd-" (u/rand-uuid)) + :appProvider (or (:appProvider body) user-id) + :appSoftVersion (or app-pkg-version "1.0.0") + :appDVersion "3.2.1" + :mecVersion "2.2.1" + ;; Minimal required MEC AppD fields + :virtualComputeDescriptor [{:virtualComputeDescId "compute-1" + :virtualCpu {:numVirtualCpu 1} + :virtualMemory {:virtualMemSize 1024}}] + :swImageDescriptor [] + :virtualStorageDescriptor [] + :appExtCpd [] + :appServiceRequired [] + :trafficRuleDescriptor [] + :dnsRuleDescriptor [] + :appFeatureRequired [] + ;; Store user-defined metadata + :userDefinedData user-defined-data}} + :identity {:user user-id + :active-claim user-id}} + + ;; Create the module + create-response (crud/add module-request) + module-id (get-in create-response [:body :resource-id]) + + ;; Retrieve the created module to get full details + module (crud/retrieve-by-id-as-admin module-id) + + ;; Convert to AppPkgInfo + app-pkg-info (-> (module->app-pkg-info module) + (assoc :onboardingState "CREATED" + :operationalState "DISABLED" + :usageState "NOT_IN_USE" + :_links {:self {:href (str "/mec/app_lcm/v2/app_packages/" module-id)} + :appPkgContent {:href (str "/mec/app_lcm/v2/app_packages/" module-id "/package_content")} + :appD {:href (str "/mec/app_lcm/v2/app_packages/" module-id "/appD")}}))] + + (log/info "Mm1: Created app_package" app-pkg-name "version" app-pkg-version "id" module-id) + + (-> (r/json-response app-pkg-info) + (assoc :status 201) + (rur/header "Location" (str "/mec/app_lcm/v2/app_packages/" module-id)))) + + (catch clojure.lang.ExceptionInfo e + (let [data (ex-data e)] + (log/warn "Mm1: Bad request creating app_package:" (.getMessage e)) + (r/json-response {:type (or (:type data) "about:blank") + :title (or (:title data) "Bad Request") + :status (or (:status data) 400) + :detail (or (:detail data) (.getMessage e))} + (or (:status data) 400)))) + + (catch Exception e + (log/error e "Mm1: Error creating app_package") + (r/json-response {:type "about:blank" + :title "Internal Server Error" + :status 500 + :detail (.getMessage e)} + 500)))) + + +;; +;; DELETE /app_packages/{appPkgId} - Delete application package +;; ETSI MEC 010-2 section 7.3.1.3.4 +;; + +(defn delete-app-package + "Delete an application package + + Path parameter: + - appPkgId: Application package identifier (module ID) + + Returns: 204 No Content on success" + [request] + (try + (let [app-pkg-id (get-in request [:params :appPkgId]) + + ;; Retrieve module to check if it exists + module (crud/retrieve-by-id-as-admin app-pkg-id) + + ;; Check if module exists + _ (when-not module + (throw (ex-info "Application package not found" + {:status 404 + :type "https://forge.etsi.org/rep/mec/gs010-2-app-pkg-lcm-api/problems/not-found" + :title "Not Found" + :detail (str "Application package " app-pkg-id " not found")}))) + + ;; Check if package is in use + _ (when (:published module) + (throw (ex-info "Cannot delete application package in use" + {:status 409 + :type "https://forge.etsi.org/rep/mec/gs010-2-app-pkg-lcm-api/problems/conflict" + :title "Conflict" + :detail "Application package is currently in use and cannot be deleted"}))) + + ;; Delete the module + delete-request {:params {:resource-name "module" + :uuid (u/id->uuid app-pkg-id)} + :identity {:user "internal" + :active-claim "internal"} + :nuvla/authn auth/internal-identity} + _ (crud/delete delete-request)] + + (log/info "Mm1: Delete app_package" app-pkg-id) + + {:status 204 + :body nil}) + + (catch clojure.lang.ExceptionInfo e + (let [data (ex-data e)] + (log/warn "Mm1: Error deleting app_package:" (.getMessage e)) + (r/json-response {:type (or (:type data) "about:blank") + :title (or (:title data) "Error") + :status (or (:status data) 500) + :detail (or (:detail data) (.getMessage e))} + (or (:status data) 500)))) + + (catch Exception e + (log/error e "Mm1: Error deleting app_package") + (r/json-response {:type "about:blank" + :title "Internal Server Error" + :status 500 + :detail (.getMessage e)} + 500)))) + + +;; +;; Route handlers +;; + +(defn routes + "Mm1 Application Package Management routes" + [] + ["/app_packages" + ["" {:get query-app-packages + :post create-app-package}] + ["/:appPkgId" {:get get-app-package + :delete delete-app-package}]]) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/error_handler.clj b/code/src/com/sixsq/nuvla/server/resources/mec/error_handler.clj new file mode 100644 index 000000000..a5e07f3bb --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/error_handler.clj @@ -0,0 +1,385 @@ +(ns com.sixsq.nuvla.server.resources.mec.error-handler + "RFC 7807 ProblemDetails Error Handler for MEC 010-2 APIs + + Provides standardized error responses compliant with RFC 7807. + All MEC API endpoints should use these error handling functions + to ensure consistent error reporting. + + Standard: RFC 7807 (Problem Details for HTTP APIs) + MEC Standard: ETSI GS MEC 010-2 v2.2.1" + (:require + [clojure.tools.logging :as log])) + + +;; +;; Error Type URIs (MEC 010-2 specific) +;; + +(def ^:const error-types + "MEC-specific error type URIs following RFC 7807" + {:not-found "https://docs.nuvla.io/mec/errors/not-found" + :validation-error "https://docs.nuvla.io/mec/errors/validation" + :conflict "https://docs.nuvla.io/mec/errors/conflict" + :unauthorized "https://docs.nuvla.io/mec/errors/unauthorized" + :forbidden "https://docs.nuvla.io/mec/errors/forbidden" + :operation-not-allowed "https://docs.nuvla.io/mec/errors/operation-not-allowed" + :invalid-state "https://docs.nuvla.io/mec/errors/invalid-state" + :resource-exhausted "https://docs.nuvla.io/mec/errors/resource-exhausted" + :mepm-error "https://docs.nuvla.io/mec/errors/mepm-error" + :internal-error "https://docs.nuvla.io/mec/errors/internal" + :timeout "https://docs.nuvla.io/mec/errors/timeout" + :bad-gateway "https://docs.nuvla.io/mec/errors/bad-gateway" + :service-unavailable "https://docs.nuvla.io/mec/errors/service-unavailable"}) + + +;; +;; Core ProblemDetails Constructor +;; + +(defn problem-details + "Creates an RFC 7807 ProblemDetails error response + + Args: + - type: URI reference identifying the problem type (keyword or string) + - title: Short, human-readable summary + - status: HTTP status code + - detail: Human-readable explanation specific to this occurrence + - instance: URI reference identifying the specific occurrence + - extensions: Map of additional problem-specific fields + + Returns: Map conforming to RFC 7807 ProblemDetails schema" + ([type title status] + (problem-details type title status nil nil nil)) + ([type title status detail] + (problem-details type title status detail nil nil)) + ([type title status detail instance] + (problem-details type title status detail instance nil)) + ([type title status detail instance extensions] + (let [type-uri (if (keyword? type) + (get error-types type "about:blank") + type)] + (cond-> {:type type-uri + :title title + :status status} + detail (assoc :detail detail) + instance (assoc :instance instance) + extensions (merge extensions))))) + + +;; +;; 4xx Client Error Responses +;; + +(defn bad-request + "400 Bad Request - Malformed request syntax" + ([detail] + (bad-request detail nil nil)) + ([detail instance] + (bad-request detail instance nil)) + ([detail instance extensions] + (problem-details + :validation-error + "Bad Request" + 400 + detail + instance + extensions))) + + +(defn unauthorized + "401 Unauthorized - Authentication required" + ([detail] + (unauthorized detail nil nil)) + ([detail instance] + (unauthorized detail instance nil)) + ([detail instance extensions] + (problem-details + :unauthorized + "Unauthorized" + 401 + detail + instance + extensions))) + + +(defn forbidden + "403 Forbidden - Insufficient permissions" + ([detail] + (forbidden detail nil nil)) + ([detail instance] + (forbidden detail instance nil)) + ([detail instance extensions] + (problem-details + :forbidden + "Forbidden" + 403 + detail + instance + extensions))) + + +(defn not-found + "404 Not Found - Resource does not exist" + ([resource-type resource-id] + (not-found resource-type resource-id nil)) + ([resource-type resource-id extensions] + (problem-details + :not-found + "Resource Not Found" + 404 + (str resource-type " " resource-id " not found") + resource-id + extensions))) + + +(defn conflict + "409 Conflict - Request conflicts with current state" + ([detail] + (conflict detail nil nil)) + ([detail instance] + (conflict detail instance nil)) + ([detail instance extensions] + (problem-details + :conflict + "Resource Conflict" + 409 + detail + instance + extensions))) + + +(defn invalid-state + "409 Conflict - Operation not allowed in current state" + [resource-id current-state expected-state operation] + (problem-details + :invalid-state + "Invalid State for Operation" + 409 + (str "Cannot perform " operation " on resource in state " current-state + ". Expected state: " expected-state) + resource-id + {:current-state current-state + :expected-state expected-state + :operation operation})) + + +(defn operation-not-allowed + "422 Unprocessable Entity - Operation not allowed" + ([detail] + (operation-not-allowed detail nil nil)) + ([detail instance] + (operation-not-allowed detail instance nil)) + ([detail instance extensions] + (problem-details + :operation-not-allowed + "Operation Not Allowed" + 422 + detail + instance + extensions))) + + +(defn resource-exhausted + "429 Too Many Requests or 507 Insufficient Storage - Resource exhausted" + [resource-type detail] + (problem-details + :resource-exhausted + "Resource Exhausted" + 507 + detail + nil + {:resource-type resource-type})) + + +;; +;; 5xx Server Error Responses +;; + +(defn internal-error + "500 Internal Server Error - Unexpected server error" + ([detail] + (internal-error detail nil nil)) + ([detail instance] + (internal-error detail instance nil)) + ([detail instance extensions] + (problem-details + :internal-error + "Internal Server Error" + 500 + detail + instance + extensions))) + + +(defn mepm-error + "502 Bad Gateway - MEPM communication error" + [mepm-endpoint detail] + (problem-details + :mepm-error + "MEPM Communication Error" + 502 + detail + nil + {:mepm-endpoint mepm-endpoint})) + + +(defn service-unavailable + "503 Service Unavailable - Service temporarily unavailable" + ([detail] + (service-unavailable detail nil nil)) + ([detail instance] + (service-unavailable detail instance nil)) + ([detail instance extensions] + (problem-details + :service-unavailable + "Service Unavailable" + 503 + detail + instance + extensions))) + + +(defn gateway-timeout + "504 Gateway Timeout - MEPM timeout" + [mepm-endpoint operation] + (problem-details + :timeout + "Gateway Timeout" + 504 + (str "Timeout waiting for MEPM response during " operation) + nil + {:mepm-endpoint mepm-endpoint + :operation operation})) + + +;; +;; Validation Error Helpers +;; + +(defn validation-error + "400 Bad Request - Schema validation error + + Args: + - field: Field name that failed validation + - reason: Why validation failed + - value: The invalid value (optional)" + ([field reason] + (validation-error field reason nil)) + ([field reason value] + (bad-request + (str "Validation failed for field '" field "': " reason) + nil + {:field field + :reason reason + :value value}))) + + +(defn missing-required-field + "400 Bad Request - Required field missing" + [field] + (validation-error + field + "Required field is missing" + nil)) + + +(defn invalid-field-value + "400 Bad Request - Invalid field value" + [field value expected] + (validation-error + field + (str "Invalid value. Expected: " expected) + value)) + + +(defn invalid-enum-value + "400 Bad Request - Invalid enum value" + [field value valid-values] + (validation-error + field + (str "Invalid enum value. Valid values: " (clojure.string/join ", " valid-values)) + value)) + + +;; +;; Exception Handling Helpers +;; + +(defn exception->problem-details + "Converts an exception to RFC 7807 ProblemDetails + + Handles: + - ExceptionInfo with :status in ex-data + - Known exception types + - Generic exceptions" + [exception & {:keys [instance operation]}] + (if-let [ex-data (when (instance? clojure.lang.ExceptionInfo exception) + (ex-data exception))] + ;; Handle ExceptionInfo with status + (let [status (:status ex-data 500) + message (ex-message exception) + type-keyword (cond + (= status 404) :not-found + (= status 409) :conflict + (= status 422) :operation-not-allowed + (>= status 500) :internal-error + :else :validation-error) + extensions (cond-> (dissoc ex-data :status) + operation (assoc :operation operation))] + (problem-details + type-keyword + (case status + 404 "Resource Not Found" + 409 "Resource Conflict" + 422 "Operation Not Allowed" + 500 "Internal Server Error" + 502 "Bad Gateway" + 503 "Service Unavailable" + "Bad Request") + status + message + instance + extensions)) + + ;; Handle generic exceptions + (do + (log/error exception "Unexpected exception during" operation) + (internal-error + (or (ex-message exception) "An unexpected error occurred") + instance + {:exception-type (str (type exception)) + :operation operation})))) + + +;; +;; Logging Helpers +;; + +(defn log-and-return-error + "Logs error details and returns ProblemDetails response + + Useful for error handling in catch blocks" + [problem-details-map operation context] + (let [status (:status problem-details-map) + title (:title problem-details-map) + detail (:detail problem-details-map)] + (if (>= status 500) + (log/error "Server error during" operation "-" title ":" detail "| Context:" context) + (log/warn "Client error during" operation "-" title ":" detail "| Context:" context)) + problem-details-map)) + + +;; +;; Testing Helper +;; + +(defn problem-details? + "Predicate to check if a map is a valid RFC 7807 ProblemDetails response" + [m] + (and (map? m) + (contains? m :type) + (contains? m :title) + (contains? m :status) + (number? (:status m)) + (>= (:status m) 400) + (< (:status m) 600))) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/lifecycle_handler.clj b/code/src/com/sixsq/nuvla/server/resources/mec/lifecycle_handler.clj new file mode 100644 index 000000000..97b34a7c3 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/lifecycle_handler.clj @@ -0,0 +1,294 @@ +(ns com.sixsq.nuvla.server.resources.mec.lifecycle-handler + "MEC 010-2 Lifecycle Operation Handler + + Handles instantiate, terminate, and operate lifecycle operations by: + 1. Validating the request + 2. Creating an operation occurrence (job) + 3. Delegating to MEPM via Mm5 interface + 4. Tracking operation status + + Standard: ETSI GS MEC 010-2 v2.2.1" + (:require + [clojure.tools.logging :as log] + [com.sixsq.nuvla.server.resources.mec.mm3-client :as mm3] + [com.sixsq.nuvla.server.resources.mec.app-instance :as app-instance] + [com.sixsq.nuvla.server.resources.mec.app-lcm-op-occ :as app-lcm-op-occ] + [com.sixsq.nuvla.server.util.time :as time-utils])) + + +;; +;; Operation Context +;; + +(defn create-operation-context + "Creates an operation context for tracking lifecycle operations" + [operation-type app-instance-id request-params] + {:operation-type (name operation-type) + :app-instance-id app-instance-id + :request-params request-params + :start-time (time-utils/now-str) + :status :STARTING}) + + +;; +;; Instantiate Operation +;; + +(defn execute-instantiate + "Executes app instantiation via MEPM Mm5 interface + + Steps: + 1. Query MEPM capabilities to find suitable host + 2. Send instantiate request to MEPM + 3. Track operation in AppLcmOpOcc + 4. Return operation occurrence ID" + [app-instance-id mepm-endpoint grant-id] + (try + (log/info "Executing instantiate operation for" app-instance-id) + + ;; Query MEPM capabilities + (let [capabilities (mm3/query-capabilities mepm-endpoint)] + (log/debug "MEPM capabilities:" capabilities) + + ;; Check if MEPM supports required capabilities + (when-not (:supports-app-instantiation capabilities) + (throw (ex-info "MEPM does not support app instantiation" + {:mepm-endpoint mepm-endpoint}))) + + ;; Create app instance via Mm3 + (let [app-instance-result (mm3/create-app-instance + mepm-endpoint + {:app-instance-id app-instance-id + :grant-id grant-id})] + (log/info "App instance created via Mm3:" (:instance-id app-instance-result)) + + ;; Return success result + {:status :PROCESSING + :instance-id (:instance-id app-instance-result) + :mepm-endpoint mepm-endpoint + :operation-state :INSTANTIATION_IN_PROGRESS})) + + (catch Exception e + (log/error e "Failed to execute instantiate operation") + {:status :FAILED + :error-detail (ex-message e)}))) + + +;; +;; Terminate Operation +;; + +(defn execute-terminate + "Executes app termination via MEPM Mm5 interface + + Steps: + 1. Query app instance status from MEPM + 2. Send terminate request to MEPM + 3. Track operation in AppLcmOpOcc + 4. Return operation occurrence ID" + [app-instance-id mepm-endpoint termination-type] + (try + (log/info "Executing terminate operation for" app-instance-id) + + ;; Get current app instance status + (let [app-status (mm3/get-app-instance mepm-endpoint app-instance-id)] + (log/debug "Current app instance status:" app-status) + + ;; Validate app instance exists + (when-not app-status + (throw (ex-info "App instance not found on MEPM" + {:app-instance-id app-instance-id + :mepm-endpoint mepm-endpoint}))) + + ;; Delete app instance via Mm5 + (let [delete-result (mm3/delete-app-instance + mepm-endpoint + app-instance-id)] + (log/info "App instance terminated via Mm5:" app-instance-id) + + ;; Return success result + {:status :PROCESSING + :instance-id app-instance-id + :mepm-endpoint mepm-endpoint + :operation-state :TERMINATION_IN_PROGRESS})) + + (catch Exception e + (log/error e "Failed to execute terminate operation") + {:status :FAILED + :error-detail (ex-message e)}))) + + +;; +;; Operate Operation +;; + +(defn execute-operate + "Executes app operate (start/stop) via MEPM Mm5 interface + + Steps: + 1. Validate target state (STARTED/STOPPED) + 2. Query current app instance status + 3. Send operate request to MEPM + 4. Track operation in AppLcmOpOcc + 5. Return operation occurrence ID" + [app-instance-id mepm-endpoint change-state-to] + (try + (log/info "Executing operate operation for" app-instance-id "to state" change-state-to) + + ;; Validate target state + (when-not (#{:STARTED :STOPPED "STARTED" "STOPPED"} change-state-to) + (throw (ex-info "Invalid changeStateTo value" + {:value change-state-to + :allowed [:STARTED :STOPPED]}))) + + ;; Get current app instance status + (let [app-status (mm3/get-app-instance mepm-endpoint app-instance-id)] + (log/debug "Current app instance status:" app-status) + + ;; Validate app instance exists + (when-not app-status + (throw (ex-info "App instance not found on MEPM" + {:app-instance-id app-instance-id + :mepm-endpoint mepm-endpoint}))) + + ;; Check if state change is needed + (let [current-state (keyword (:operational-state app-status)) + target-state (keyword change-state-to)] + (when (= current-state target-state) + (log/warn "App instance already in target state:" target-state) + (throw (ex-info "App instance already in target state" + {:current-state current-state + :target-state target-state}))) + + ;; Execute state change via Mm5 + ;; Note: This would require an Mm5 operate endpoint (future enhancement) + (log/info "App operate request would be sent to Mm5 (not yet implemented)") + + ;; Return success result + {:status :PROCESSING + :instance-id app-instance-id + :mepm-endpoint mepm-endpoint + :current-state current-state + :target-state target-state + :operation-state :OPERATION_IN_PROGRESS})) + + (catch Exception e + (log/error e "Failed to execute operate operation") + {:status :FAILED + :error-detail (ex-message e)}))) + + +;; +;; Operation Orchestration +;; + +(defn handle-lifecycle-operation + "Main handler for lifecycle operations + + Orchestrates the complete lifecycle operation flow: + 1. Create operation context + 2. Execute operation via appropriate handler + 3. Create operation occurrence for tracking + 4. Return operation occurrence info" + [operation-type app-instance-id mepm-endpoint request-params] + (try + ;; Create operation context + (let [context (create-operation-context operation-type app-instance-id request-params)] + (log/info "Starting lifecycle operation:" operation-type "for" app-instance-id) + + ;; Execute operation based on type + (let [result (case operation-type + :INSTANTIATE (execute-instantiate + app-instance-id + mepm-endpoint + (:grantId request-params)) + + :TERMINATE (execute-terminate + app-instance-id + mepm-endpoint + (:terminationType request-params "FORCEFUL")) + + :OPERATE (execute-operate + app-instance-id + mepm-endpoint + (:changeStateTo request-params)) + + (throw (ex-info "Unknown operation type" + {:operation-type operation-type})))] + + ;; Create operation occurrence for tracking + (let [op-occ-id (str "job/" (java.util.UUID/randomUUID)) + op-occ {:lcmOpOccId op-occ-id + :operationType (name operation-type) + :operationState (if (= :FAILED (:status result)) + "FAILED" + "PROCESSING") + :stateEnteredTime (time-utils/now-str) + :startTime (:start-time context) + :appInstanceId app-instance-id + :_links {:self {:href (str "/app_lcm/v2/app_lcm_op_occs/" op-occ-id)} + :appInstance {:href (str "/app_lcm/v2/app_instances/" app-instance-id)}}}] + + ;; Add error if operation failed + (if (= :FAILED (:status result)) + (assoc op-occ :error {:type "about:blank" + :title "Operation Failed" + :status 500 + :detail (:error-detail result) + :instance op-occ-id}) + op-occ)))) + + (catch Exception e + (log/error e "Failed to handle lifecycle operation") + (throw e)))) + + +;; +;; MEPM Endpoint Resolution +;; + +(defn resolve-mepm-endpoint + "Resolves the MEPM endpoint for a given app instance + + Strategy: + 1. Check if app instance has assigned MEPM + 2. Query available MEPMs + 3. Select optimal MEPM based on capabilities and resources + 4. Return MEPM endpoint URL" + [app-instance-id] + (try + ;; For now, return a default MEPM endpoint + ;; In production, this would query the MEPM registry + (let [default-mepm-endpoint "http://localhost:8080/mepm"] + (log/debug "Resolved MEPM endpoint for" app-instance-id ":" default-mepm-endpoint) + default-mepm-endpoint) + + (catch Exception e + (log/error e "Failed to resolve MEPM endpoint") + (throw (ex-info "No suitable MEPM found" + {:app-instance-id app-instance-id}))))) + + +;; +;; Public API +;; + +(defn instantiate + "Public API for app instantiation" + [app-instance-id request-params] + (let [mepm-endpoint (resolve-mepm-endpoint app-instance-id)] + (handle-lifecycle-operation :INSTANTIATE app-instance-id mepm-endpoint request-params))) + + +(defn terminate + "Public API for app termination" + [app-instance-id request-params] + (let [mepm-endpoint (resolve-mepm-endpoint app-instance-id)] + (handle-lifecycle-operation :TERMINATE app-instance-id mepm-endpoint request-params))) + + +(defn operate + "Public API for app operate (start/stop)" + [app-instance-id request-params] + (let [mepm-endpoint (resolve-mepm-endpoint app-instance-id)] + (handle-lifecycle-operation :OPERATE app-instance-id mepm-endpoint request-params))) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/mm3_client.clj b/code/src/com/sixsq/nuvla/server/resources/mec/mm3_client.clj new file mode 100644 index 000000000..3285606d2 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/mm3_client.clj @@ -0,0 +1,467 @@ +(ns com.sixsq.nuvla.server.resources.mec.mm3-client + "Mm3 Interface Client - MEO to MEPM communication + + ETSI MEC 003 Reference Point Mm3: + - Interface between MEC Orchestrator (MEO) and MEC Platform Manager (MEPM) + - Used for management of application lifecycle, application rules and requirements + - Handles platform management operations: + * Platform health checks + * Capability queries + * Resource availability queries + * Platform configuration + * Application lifecycle management + + This is a REST-based client implementation." + (:require + [clj-http.client :as http] + [clojure.tools.logging :as log] + [jsonista.core :as json])) + + +;; +;; Configuration +;; + +(def ^:private default-timeout-ms + "Default HTTP timeout in milliseconds" + 30000) + +(def ^:private default-connect-timeout-ms + "Default HTTP connection timeout in milliseconds" + 10000) + +(def ^:private default-retry-attempts + "Default number of retry attempts for failed requests" + 3) + +(def ^:private retry-delay-ms + "Delay between retry attempts in milliseconds" + 1000) + + +;; +;; HTTP Client Utilities +;; + +(defn- build-http-options + "Build HTTP client options with standard settings" + [endpoint {:keys [timeout connect-timeout insecure?] + :or {timeout default-timeout-ms + connect-timeout default-connect-timeout-ms + insecure? false}}] + {:socket-timeout timeout + :connection-timeout connect-timeout + :insecure? insecure? + :throw-exceptions false + :as :json + :content-type :json + :accept :json + :coerce :always}) + + +(defn- parse-response + "Parse HTTP response and handle errors" + [{:keys [status body] :as response}] + (cond + ;; Success + (and (>= status 200) (< status 300)) + {:success? true + :status status + :data body} + + ;; Client error (4xx) + (and (>= status 400) (< status 500)) + {:success? false + :status status + :error :client-error + :message (or (:message body) + (str "Client error: " status))} + + ;; Server error (5xx) + (>= status 500) + {:success? false + :status status + :error :server-error + :message (or (:message body) + (str "Server error: " status))} + + ;; Unknown error + :else + {:success? false + :status status + :error :unknown-error + :message "Unknown error occurred"})) + + +(defn- retry-request + "Retry a request with exponential backoff" + [request-fn max-attempts] + (loop [attempt 1] + (let [result (try + (request-fn) + (catch Exception e + {:success? false + :error :exception + :message (.getMessage e) + :exception e}))] + (if (or (:success? result) + (>= attempt max-attempts)) + result + (do + (log/debug "Retry attempt" attempt "/" max-attempts) + (Thread/sleep (* retry-delay-ms attempt)) + (recur (inc attempt))))))) + + +;; +;; Mm3 API Operations +;; + +(defn check-health + "Perform health check on MEPM via Mm3 interface. + + ETSI MEC 003: Mm3 health check operation + - Verifies MEPM is reachable and operational + - Returns platform status and metrics + + Parameters: + - endpoint: MEPM base URL (e.g., 'https://mepm.example.com:8443') + - options: HTTP client options (optional) + * :timeout - request timeout in ms (default: 30000) + * :connect-timeout - connection timeout in ms (default: 10000) + * :insecure? - allow insecure SSL (default: false) + * :retry-attempts - number of retries (default: 3) + + Returns: + - {:success? true :status 200 :data {...}} on success + - {:success? false :error :xxx :message \"...\"} on failure" + [endpoint & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Checking health of MEPM at" endpoint) + (let [url (str endpoint "/mm3/health") + http-opts (build-http-options endpoint options)] + (retry-request + (fn [] + (try + (let [response (http/get url http-opts)] + (log/debug "Mm3 health check response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to check health") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +(defn query-capabilities + "Query MEPM capabilities via Mm3 interface. + + ETSI MEC 003: Mm3 capability query operation + - Retrieves supported platforms, services, and API versions + - Used for service discovery and compatibility checks + + Parameters: + - endpoint: MEPM base URL + - options: HTTP client options (optional) + + Returns: + - {:success? true :data {:platforms [...] :services [...] :api-version \"...\"}} + - {:success? false :error :xxx :message \"...\"}" + [endpoint & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Querying capabilities from MEPM at" endpoint) + (let [url (str endpoint "/mm3/capabilities") + http-opts (build-http-options endpoint options)] + (retry-request + (fn [] + (try + (let [response (http/get url http-opts)] + (log/debug "Mm3 capabilities response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to query capabilities") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +(defn query-resources + "Query available resources on MEPM via Mm3 interface. + + ETSI MEC 003: Mm3 resource query operation + - Retrieves available compute, memory, storage, and GPU resources + - Used for placement decisions and capacity planning + + Parameters: + - endpoint: MEPM base URL + - options: HTTP client options (optional) + + Returns: + - {:success? true :data {:cpu-cores N :memory-gb N :storage-gb N :gpu-count N}} + - {:success? false :error :xxx :message \"...\"}" + [endpoint & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Querying resources from MEPM at" endpoint) + (let [url (str endpoint "/mm3/resources") + http-opts (build-http-options endpoint options)] + (retry-request + (fn [] + (try + (let [response (http/get url http-opts)] + (log/debug "Mm3 resources response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to query resources") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +(defn configure-platform + "Configure MEPM platform settings via Mm3 interface. + + ETSI MEC 003: Mm3 platform configuration operation + - Updates platform-level settings + - Configures enabled services and features + + Parameters: + - endpoint: MEPM base URL + - config: Configuration map (e.g., {:service-registry true}) + - options: HTTP client options (optional) + + Returns: + - {:success? true :status 200} + - {:success? false :error :xxx :message \"...\"}" + [endpoint config & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Configuring MEPM at" endpoint "with config:" config) + (let [url (str endpoint "/mm3/configure") + http-opts (merge (build-http-options endpoint options) + {:body (json/write-value-as-string config)})] + (retry-request + (fn [] + (try + (let [response (http/post url http-opts)] + (log/debug "Mm3 configure response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to configure platform") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +(defn get-platform-info + "Get general platform information via Mm3 interface. + + ETSI MEC 003: Mm3 platform info operation + - Retrieves platform metadata and status + - Includes version, location, and operational state + + Parameters: + - endpoint: MEPM base URL + - options: HTTP client options (optional) + + Returns: + - {:success? true :data {:name \"...\" :version \"...\" :status \"...\"}} + - {:success? false :error :xxx :message \"...\"}" + [endpoint & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Getting platform info from MEPM at" endpoint) + (let [url (str endpoint "/mm3/platform-info") + http-opts (build-http-options endpoint options)] + (retry-request + (fn [] + (try + (let [response (http/get url http-opts)] + (log/debug "Mm3 platform info response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to get platform info") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +;; +;; Application Lifecycle Operations +;; + +(defn create-app-instance + "Create a new application instance via Mm3 interface. + + ETSI MEC 003: Mm3 application instantiation operation + + Parameters: + - endpoint: MEPM base URL + - app-descriptor: Application descriptor map + - options: HTTP client options (optional) + + Returns: + - {:success? true :status 201 :data {:id \"...\" :status \"...\"}} + - {:success? false :error :xxx :message \"...\"}" + [endpoint app-descriptor & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Creating app instance on MEPM at" endpoint) + (let [url (str endpoint "/mm3/app-instances") + http-opts (build-http-options endpoint options) + http-opts (assoc http-opts :body (json/write-value-as-string app-descriptor) + :content-type :json)] + (retry-request + (fn [] + (try + (let [response (http/post url http-opts)] + (log/debug "Mm3 create app instance response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to create app instance") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +(defn get-app-instance + "Get application instance status via Mm3 interface. + + ETSI MEC 003: Mm3 application query operation + + Parameters: + - endpoint: MEPM base URL + - app-id: Application instance identifier + - options: HTTP client options (optional) + + Returns: + - {:success? true :status 200 :data {:id \"...\" :status \"...\"}} + - {:success? false :error :xxx :message \"...\"}" + [endpoint app-id & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Getting app instance" app-id "from MEPM at" endpoint) + (let [url (str endpoint "/mm3/app-instances/" app-id) + http-opts (build-http-options endpoint options)] + (retry-request + (fn [] + (try + (let [response (http/get url http-opts)] + (log/debug "Mm3 get app instance response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to get app instance") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +(defn list-app-instances + "List all application instances via Mm3 interface. + + ETSI MEC 003: Mm3 application listing operation + + Parameters: + - endpoint: MEPM base URL + - options: HTTP client options (optional) + + Returns: + - {:success? true :status 200 :data {:instances [...]}} + - {:success? false :error :xxx :message \"...\"}" + [endpoint & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Listing app instances from MEPM at" endpoint) + (let [url (str endpoint "/mm3/app-instances") + http-opts (build-http-options endpoint options)] + (retry-request + (fn [] + (try + (let [response (http/get url http-opts)] + (log/debug "Mm3 list app instances response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to list app instances") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +(defn delete-app-instance + "Delete (terminate) an application instance via Mm3 interface. + + ETSI MEC 003: Mm3 application termination operation + + Parameters: + - endpoint: MEPM base URL + - app-id: Application instance identifier + - options: HTTP client options (optional) + + Returns: + - {:success? true :status 204} + - {:success? false :error :xxx :message \"...\"}" + [endpoint app-id & [{:keys [retry-attempts] + :or {retry-attempts default-retry-attempts} + :as options}]] + (log/info "Mm3: Deleting app instance" app-id "from MEPM at" endpoint) + (let [url (str endpoint "/mm3/app-instances/" app-id) + http-opts (build-http-options endpoint options)] + (retry-request + (fn [] + (try + (let [response (http/delete url http-opts)] + (log/debug "Mm3 delete app instance response:" response) + (parse-response response)) + (catch Exception e + (log/error e "Mm3: Failed to delete app instance") + {:success? false + :error :connection-error + :message (.getMessage e) + :exception e}))) + retry-attempts))) + + +;; +;; Convenience functions +;; + +(defn healthy? + "Check if MEPM is healthy (returns true/false)" + [endpoint & [options]] + (let [result (check-health endpoint options)] + (and (:success? result) + (= 200 (:status result))))) + + +(defn get-capabilities + "Get capabilities or nil if unavailable" + [endpoint & [options]] + (let [result (query-capabilities endpoint options)] + (when (:success? result) + (:data result)))) + + +(defn get-resources + "Get resources or nil if unavailable" + [endpoint & [options]] + (let [result (query-resources endpoint options)] + (when (:success? result) + (:data result)))) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/notification_dispatcher.clj b/code/src/com/sixsq/nuvla/server/resources/mec/notification_dispatcher.clj new file mode 100644 index 000000000..4f5e2dda4 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/notification_dispatcher.clj @@ -0,0 +1,387 @@ +(ns com.sixsq.nuvla.server.resources.mec.notification-dispatcher + "MEC 010-2 Notification Dispatcher + + Dispatches lifecycle notifications to subscribers via HTTP webhooks. + Monitors app instance and operation occurrence state changes and sends + notifications to matching subscriptions. + + Features: + - Event-driven notification dispatch + - Subscription filter matching + - HTTP webhook delivery with retries + - Failure tracking and logging + - Async non-blocking delivery" + (:require + [clj-http.client :as http] + [clojure.tools.logging :as log] + [com.sixsq.nuvla.server.resources.mec.app-lcm-subscription :as subscription] + [jsonista.core :as json])) + + +;; +;; Configuration +;; + +(def ^:private default-timeout-ms + "Default HTTP timeout for webhook delivery" + 30000) + +(def ^:private default-connect-timeout-ms + "Default HTTP connection timeout" + 10000) + +(def ^:private default-retry-attempts + "Default number of retry attempts" + 3) + +(def ^:private retry-delay-ms + "Delay between retry attempts in milliseconds" + 2000) + +(def ^:private max-retry-delay-ms + "Maximum retry delay (exponential backoff cap)" + 30000) + + +;; +;; Delivery Statistics +;; + +(def delivery-stats (atom {:total-sent 0 + :successful 0 + :failed 0 + :retries 0})) + + +(defn get-delivery-stats + "Get current delivery statistics. + + Returns: + Map with :total-sent, :successful, :failed, :retries counts" + [] + @delivery-stats) + + +(defn reset-delivery-stats! + "Reset delivery statistics to zero" + [] + (reset! delivery-stats {:total-sent 0 + :successful 0 + :failed 0 + :retries 0})) + + +;; +;; HTTP Client +;; + +(defn- build-webhook-request + "Build HTTP request for webhook delivery" + [callback-uri notification] + {:url callback-uri + :method :post + :content-type :json + :accept :json + :body (json/write-value-as-string notification) + :socket-timeout default-timeout-ms + :conn-timeout default-connect-timeout-ms + :throw-exceptions false}) + + +(defn- calculate-retry-delay + "Calculate retry delay with exponential backoff" + [attempt] + (let [base-delay retry-delay-ms + exponential-delay (* base-delay (Math/pow 2 (dec attempt)))] + (int (min max-retry-delay-ms exponential-delay)))) + + +(defn- deliver-webhook + "Deliver notification via HTTP POST to callback URI. + + Parameters: + - callback-uri: Target webhook URL + - notification: Notification payload + - attempt: Current retry attempt (1-based) + + Returns: + - {:success? true :status } on success + - {:success? false :error :status } on failure" + [callback-uri notification attempt] + (try + (let [request (build-webhook-request callback-uri notification) + response (http/request request) + status (:status response)] + + (if (and (>= status 200) (< status 300)) + (do + (log/info "Webhook delivered successfully to" callback-uri + "- Status:" status + "- Attempt:" attempt) + {:success? true + :status status}) + (do + (log/warn "Webhook delivery failed to" callback-uri + "- Status:" status + "- Attempt:" attempt) + {:success? false + :status status + :error :http-error + :message (str "HTTP " status)}))) + + (catch java.net.ConnectException e + (log/warn "Connection failed to" callback-uri "- Attempt:" attempt + "- Error:" (.getMessage e)) + {:success? false + :error :connection-error + :message (.getMessage e)}) + + (catch java.net.SocketTimeoutException e + (log/warn "Timeout delivering to" callback-uri "- Attempt:" attempt) + {:success? false + :error :timeout + :message "Connection timeout"}) + + (catch Exception e + (log/error e "Unexpected error delivering webhook to" callback-uri + "- Attempt:" attempt) + {:success? false + :error :unexpected-error + :message (.getMessage e)}))) + + +(defn- deliver-with-retries + "Deliver notification with retry logic. + + Parameters: + - callback-uri: Target webhook URL + - notification: Notification payload + - max-attempts: Maximum retry attempts + + Returns: + Final delivery result after all attempts" + [callback-uri notification max-attempts] + (loop [attempt 1] + (let [result (deliver-webhook callback-uri notification attempt)] + (cond + ;; Success - return immediately + (:success? result) + result + + ;; Failed but can retry + (< attempt max-attempts) + (do + (swap! delivery-stats update :retries inc) + (let [delay (calculate-retry-delay attempt)] + (log/info "Retrying webhook delivery to" callback-uri + "in" delay "ms - Attempt" (inc attempt) "of" max-attempts) + (Thread/sleep delay) + (recur (inc attempt)))) + + ;; Failed with no retries left + :else + (do + (log/error "Webhook delivery failed after" max-attempts "attempts to" callback-uri) + result))))) + + +;; +;; Notification Dispatch +;; + +(defn dispatch-notification + "Dispatch notification to a single subscription. + + Parameters: + - subscription: Subscription resource + - notification: Notification payload + + Returns: + Delivery result map" + [subscription notification] + (let [callback-uri (:callback-uri subscription) + sub-id (:id subscription)] + + (log/info "Dispatching" (:notification-type notification) + "to subscription" sub-id + "- Callback:" callback-uri) + + (swap! delivery-stats update :total-sent inc) + + (let [result (deliver-with-retries callback-uri notification default-retry-attempts)] + (if (:success? result) + (swap! delivery-stats update :successful inc) + (swap! delivery-stats update :failed inc)) + + result))) + + +(defn dispatch-notification-async + "Dispatch notification asynchronously (non-blocking). + + Parameters: + - subscription: Subscription resource + - notification: Notification payload + + Returns: + Future that will contain the delivery result" + [subscription notification] + (future + (dispatch-notification subscription notification))) + + +;; +;; Event Handling +;; + +(defn handle-app-instance-state-change + "Handle app instance state change event. + + Finds matching subscriptions and dispatches notifications. + + Parameters: + - subscriptions: Collection of all subscriptions + - app-instance: Current app instance state + - change-type: Type of change (INSTANTIATION_STATE, OPERATIONAL_STATE, CONFIGURATION) + - previous-state: Previous state before change + + Returns: + Vector of dispatch futures" + [subscriptions app-instance change-type previous-state] + (let [active-subs (subscription/get-active-subscriptions-for-type + subscriptions + "AppInstanceStateChangeNotification") + + matching-subs (filter #(subscription/matches-app-instance-filter? % app-instance) + active-subs)] + + (log/info "App instance" (:id app-instance) "state changed -" + change-type "- Matching subscriptions:" (count matching-subs)) + + (mapv (fn [sub] + (let [notification (subscription/build-app-instance-notification + sub + app-instance + change-type + previous-state)] + (dispatch-notification-async sub notification))) + matching-subs))) + + +(defn handle-app-lcm-op-occ-state-change + "Handle app LCM operation occurrence state change event. + + Finds matching subscriptions and dispatches notifications. + + Parameters: + - subscriptions: Collection of all subscriptions + - app-lcm-op-occ: Current operation occurrence state + - change-type: Type of change (OPERATION_STATE, OPERATION_RESULT) + - previous-state: Previous state before change + + Returns: + Vector of dispatch futures" + [subscriptions app-lcm-op-occ change-type previous-state] + (let [active-subs (subscription/get-active-subscriptions-for-type + subscriptions + "AppLcmOpOccStateChangeNotification") + + matching-subs (filter #(subscription/matches-app-lcm-op-occ-filter? % app-lcm-op-occ) + active-subs)] + + (log/info "Operation" (:id app-lcm-op-occ) "state changed -" + change-type "- Matching subscriptions:" (count matching-subs)) + + (mapv (fn [sub] + (let [notification (subscription/build-app-lcm-op-occ-notification + sub + app-lcm-op-occ + change-type + previous-state)] + (dispatch-notification-async sub notification))) + matching-subs))) + + +;; +;; Kafka Event Integration (Stub) +;; + +(defn start-event-listener + "Start listening to Kafka events for lifecycle changes. + + This is a stub for future Kafka integration. In production, this would: + 1. Subscribe to relevant Kafka topics (deployment events, job events) + 2. Parse events and detect state changes + 3. Call handle-app-instance-state-change or handle-app-lcm-op-occ-state-change + 4. Log event processing statistics + + Parameters: + - subscription-store: Atom containing subscription collection + - opts: Configuration options + * :kafka-brokers - Kafka broker addresses + * :topics - Topics to subscribe to + * :group-id - Consumer group ID + + Returns: + Event listener handle (for stopping)" + [subscription-store opts] + (log/info "Starting MEC notification event listener (stub)") + (log/info "Kafka configuration:" (select-keys opts [:kafka-brokers :topics :group-id])) + + ;; TODO: Implement actual Kafka consumer + ;; For now, return a stub handle + {:type :stub + :started-at (java.time.Instant/now) + :subscription-store subscription-store + :opts opts}) + + +(defn stop-event-listener + "Stop event listener and clean up resources. + + Parameters: + - listener-handle: Handle returned from start-event-listener + + Returns: + nil" + [listener-handle] + (log/info "Stopping MEC notification event listener") + (when (= (:type listener-handle) :stub) + (log/info "Stub listener stopped")) + nil) + + +;; +;; Manual Event Triggering (for testing) +;; + +(defn trigger-app-instance-notification + "Manually trigger app instance notification (for testing). + + Parameters: + - subscriptions: Collection of subscriptions + - app-instance: App instance resource + - change-type: Type of change + - previous-state: Previous state (optional) + + Returns: + Vector of delivery futures" + [subscriptions app-instance change-type previous-state] + (log/info "Manually triggering app instance notification") + (handle-app-instance-state-change subscriptions app-instance change-type previous-state)) + + +(defn trigger-app-lcm-op-occ-notification + "Manually trigger operation occurrence notification (for testing). + + Parameters: + - subscriptions: Collection of subscriptions + - app-lcm-op-occ: Operation occurrence resource + - change-type: Type of change + - previous-state: Previous state (optional) + + Returns: + Vector of delivery futures" + [subscriptions app-lcm-op-occ change-type previous-state] + (log/info "Manually triggering operation occurrence notification") + (handle-app-lcm-op-occ-state-change subscriptions app-lcm-op-occ change-type previous-state)) diff --git a/code/src/com/sixsq/nuvla/server/resources/mec/query_filter.clj b/code/src/com/sixsq/nuvla/server/resources/mec/query_filter.clj new file mode 100644 index 000000000..138871d0d --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mec/query_filter.clj @@ -0,0 +1,364 @@ +(ns com.sixsq.nuvla.server.resources.mec.query-filter + "MEC 010-2 Query Filter Parser + + Implements FIQL-like query filter parsing for MEC API endpoints. + Supports filtering collections by attribute values. + + Filter Syntax: + - Equality: filter=(eq,field,value) + - Not equal: filter=(neq,field,value) + - Greater than: filter=(gt,field,value) + - Less than: filter=(lt,field,value) + - In set: filter=(in,field,value1,value2,value3) + - And: filter=(and,(eq,field1,value1),(eq,field2,value2)) + - Or: filter=(or,(eq,field1,value1),(eq,field2,value2)) + + Example: + GET /app_instances?filter=(eq,appName,my-app) + GET /app_instances?filter=(and,(eq,operationalState,STARTED),(neq,appName,test))" + (:require + [clojure.string :as str] + [clojure.tools.logging :as log])) + + +;; +;; Filter Parser +;; + +(defn- tokenize + "Tokenize filter string into components. + Split by commas but respect parentheses nesting." + [s] + (loop [chars (seq s) + tokens [] + current [] + depth 0] + (if-let [c (first chars)] + (cond + ;; Open paren increases depth + (= c \() + (recur (rest chars) tokens (conj current c) (inc depth)) + + ;; Close paren decreases depth + (= c \)) + (recur (rest chars) tokens (conj current c) (dec depth)) + + ;; Comma at depth 0 is a separator + (and (= c \,) (zero? depth)) + (recur (rest chars) (conj tokens (str/join current)) [] 0) + + ;; Any other character + :else + (recur (rest chars) tokens (conj current c) depth)) + + ;; End of string + (if (seq current) + (conj tokens (str/join current)) + tokens)))) + + +(defn- parse-filter-expr + "Parse a single filter expression. + Returns a map with :op, :field, :value(s)" + [expr] + (let [trimmed (str/trim expr)] + (if (str/starts-with? trimmed "(") + ;; Expression is wrapped in parens + (let [inner (subs trimmed 1 (dec (count trimmed))) + tokens (tokenize inner) + op (first tokens)] + (case op + "eq" + {:op :eq + :field (keyword (second tokens)) + :value (nth tokens 2)} + + "neq" + {:op :neq + :field (keyword (second tokens)) + :value (nth tokens 2)} + + "gt" + {:op :gt + :field (keyword (second tokens)) + :value (nth tokens 2)} + + "lt" + {:op :lt + :field (keyword (second tokens)) + :value (nth tokens 2)} + + "gte" + {:op :gte + :field (keyword (second tokens)) + :value (nth tokens 2)} + + "lte" + {:op :lte + :field (keyword (second tokens)) + :value (nth tokens 2)} + + "in" + {:op :in + :field (keyword (second tokens)) + :values (vec (drop 2 tokens))} + + "and" + {:op :and + :exprs (mapv parse-filter-expr (rest tokens))} + + "or" + {:op :or + :exprs (mapv parse-filter-expr (rest tokens))} + + ;; Unknown operator + (throw (ex-info "Unknown filter operator" + {:operator op + :expression expr})))) + + ;; Not wrapped in parens, treat as literal + {:op :literal + :value trimmed}))) + + +(defn parse-filter + "Parse a filter query string into a filter expression. + + Parameters: + - filter-str: Filter query string + + Returns: + Filter expression map or nil if empty/invalid" + [filter-str] + (when (and filter-str (not (str/blank? filter-str))) + (try + (parse-filter-expr filter-str) + (catch Exception e + (log/warn "Failed to parse filter:" filter-str "-" (.getMessage e)) + nil)))) + + +;; +;; Filter Evaluation +;; + +(defn- coerce-value + "Coerce string value to appropriate type for comparison" + [v] + (cond + ;; Try integer + (re-matches #"-?\d+" v) + (Long/parseLong v) + + ;; Try boolean + (= "true" v) + true + + (= "false" v) + false + + ;; Keep as string + :else + v)) + + +(defn- compare-values + "Compare two values with type coercion" + [op v1 v2] + (let [cv1 (if (string? v1) (coerce-value v1) v1) + cv2 (if (string? v2) (coerce-value v2) v2)] + (try + (case op + :eq (= cv1 cv2) + :neq (not= cv1 cv2) + :gt (> (compare cv1 cv2) 0) + :lt (< (compare cv1 cv2) 0) + :gte (>= (compare cv1 cv2) 0) + :lte (<= (compare cv1 cv2) 0) + false) + (catch Exception e + (log/debug "Comparison failed:" cv1 op cv2 "-" (.getMessage e)) + false)))) + + +(defn- evaluate-filter-expr + "Evaluate a filter expression against a resource. + + Parameters: + - expr: Parsed filter expression + - resource: Resource map to evaluate against + + Returns: + Boolean indicating if resource matches filter" + [expr resource] + (case (:op expr) + :eq + (compare-values :eq (get resource (:field expr)) (:value expr)) + + :neq + (compare-values :neq (get resource (:field expr)) (:value expr)) + + :gt + (compare-values :gt (get resource (:field expr)) (:value expr)) + + :lt + (compare-values :lt (get resource (:field expr)) (:value expr)) + + :gte + (compare-values :gte (get resource (:field expr)) (:value expr)) + + :lte + (compare-values :lte (get resource (:field expr)) (:value expr)) + + :in + (let [field-val (get resource (:field expr)) + coerced-vals (map coerce-value (:values expr))] + (some #(= field-val %) coerced-vals)) + + :and + (every? #(evaluate-filter-expr % resource) (:exprs expr)) + + :or + (some #(evaluate-filter-expr % resource) (:exprs expr)) + + :literal + true ; Literal expressions always match + + ;; Unknown operator + false)) + + +(defn apply-filter + "Apply filter expression to a collection of resources. + + Parameters: + - filter-expr: Parsed filter expression (from parse-filter) + - resources: Collection of resource maps + + Returns: + Filtered collection" + [filter-expr resources] + (if filter-expr + (filter #(evaluate-filter-expr filter-expr %) resources) + resources)) + + +;; +;; Pagination +;; + +(defn- build-page-link + "Build a pagination link" + [base-uri page size] + {:href (str base-uri "?page=" page "&size=" size)}) + + +(defn paginate + "Apply pagination to a collection with HAL-style links. + + Parameters: + - resources: Collection of resources + - opts: Pagination options + * :page - Page number (1-based, default 1) + * :size - Page size (default 20) + * :base-uri - Base URI for links (default '') + + Returns: + Map with :items, :total, :page, :size, :_links" + [resources {:keys [page size base-uri] + :or {page 1 size 20 base-uri ""}}] + (let [total (count resources) + page (max 1 page) + size (max 1 (min size 100)) ; Cap at 100 + offset (* (dec page) size) + total-pages (int (Math/ceil (/ total (double size)))) + items (vec (take size (drop offset resources))) + + ;; HAL-style links + links {:self (build-page-link base-uri page size)} + links (if (> page 1) + (assoc links + :first (build-page-link base-uri 1 size) + :prev (build-page-link base-uri (dec page) size)) + links) + links (if (< page total-pages) + (assoc links + :next (build-page-link base-uri (inc page) size) + :last (build-page-link base-uri total-pages size)) + links)] + + {:items items + :total total + :page page + :size size + :totalPages total-pages + :_links links})) + + +;; +;; Field Selection +;; + +(defn parse-fields + "Parse fields parameter into a set of keywords. + + Parameters: + - fields-str: Comma-separated field names (e.g., 'appName,operationalState') + + Returns: + Set of field keywords or nil for all fields" + [fields-str] + (when (and fields-str (not (str/blank? fields-str))) + (set (map keyword (str/split fields-str #","))))) + + +(defn select-fields + "Select specified fields from resources. + + Parameters: + - fields: Set of field keywords (from parse-fields) or nil for all + - resources: Collection of resource maps + + Returns: + Collection with only selected fields" + [fields resources] + (if fields + (map #(select-keys % (conj fields :id)) resources) ; Always include :id + resources)) + + +;; +;; Combined Query Processing +;; + +(defn process-query + "Process query parameters: filter, paginate, and select fields. + + Parameters: + - resources: Collection of resources to query + - query-params: Map of query parameters + * :filter - Filter expression string + * :page - Page number + * :size - Page size + * :fields - Comma-separated field names + * :base-uri - Base URI for pagination links + + Returns: + Map with :items, :total, :page, :size, :_links" + [resources query-params] + (let [{:keys [filter page size fields base-uri]} query-params + + ;; Parse parameters + filter-expr (parse-filter filter) + field-set (parse-fields fields) + page (or (some-> page Integer/parseInt) 1) + size (or (some-> size Integer/parseInt) 20) + + ;; Apply operations in order: filter -> select fields -> paginate + filtered (apply-filter filter-expr resources) + selected (select-fields field-set filtered) + result (paginate selected {:page page + :size size + :base-uri (or base-uri "")})] + + result)) diff --git a/code/src/com/sixsq/nuvla/server/resources/mepm.clj b/code/src/com/sixsq/nuvla/server/resources/mepm.clj new file mode 100644 index 000000000..8fbb535f2 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/mepm.clj @@ -0,0 +1,282 @@ +(ns com.sixsq.nuvla.server.resources.mepm + " +MEC Platform Manager (MEPM) resource represents an external platform manager +that Nuvla (MEO) communicates with via the Mm5 interface as defined in +ETSI GS MEC 003. + +MEPMs manage host-level operations such as: +- Application lifecycle on specific MEC hosts +- Platform service configuration +- Resource management at the host level + +This resource enables Nuvla to act as a MEC Orchestrator (MEO) that coordinates +with multiple MEPMs across distributed edge infrastructure. +" + (:require + [clojure.tools.logging :as log] + [com.sixsq.nuvla.auth.acl-resource :as a] + [com.sixsq.nuvla.auth.utils :as auth] + [com.sixsq.nuvla.db.impl :as db] + [com.sixsq.nuvla.server.resources.common.crud :as crud] + [com.sixsq.nuvla.server.resources.common.event-config :as ec] + [com.sixsq.nuvla.server.resources.common.event-context :as ectx] + [com.sixsq.nuvla.server.resources.common.std-crud :as std-crud] + [com.sixsq.nuvla.server.resources.common.utils :as u] + [com.sixsq.nuvla.server.resources.mec.mm3-client :as mm3] + [com.sixsq.nuvla.server.resources.resource-metadata :as md] + [com.sixsq.nuvla.server.resources.spec.mepm :as mepm-spec] + [com.sixsq.nuvla.server.util.metadata :as gen-md] + [com.sixsq.nuvla.server.util.response :as r] + [com.sixsq.nuvla.server.util.time :as time])) + + +(def ^:const resource-type (u/ns->type *ns*)) + + +(def ^:const collection-type (u/ns->collection-type *ns*)) + + +;; Only authenticated users can view and manage MEPMs +(def collection-acl {:query ["group/nuvla-user"] + :add ["group/nuvla-user"]}) + + +;; +;; Events +;; + +(defmethod ec/events-enabled? resource-type + [_resource-type] + true) + +(defmethod ec/log-event? "mepm.add" + [_event _response] + true) + +(defmethod ec/log-event? "mepm.edit" + [_event _response] + true) + +(defmethod ec/log-event? "mepm.delete" + [_event _response] + true) + + +;; +;; Resource metadata +;; + +(def resource-metadata (gen-md/generate-metadata ::ns ::mepm-spec/schema)) + + +;; +;; Initialization +;; + +(def initialization-order 120) + +(defn initialize + [] + (std-crud/initialize resource-type ::mepm-spec/schema) + (md/register resource-metadata)) + + +;; +;; Validation +;; + +(def validate-fn (u/create-spec-validation-fn ::mepm-spec/schema)) + +(defmethod crud/validate resource-type + [resource] + (validate-fn resource)) + + +;; +;; CRUD operations +;; + +(def add-impl (std-crud/add-fn resource-type collection-acl resource-type)) + +(defmethod crud/add resource-type + [{{:keys [name endpoint capabilities status] :as body} :body :as request}] + (let [authn-info (auth/current-authentication request) + current-user (auth/current-user-id request) + desc-attr (u/select-desc-keys body) + mepm-resource (cond-> (merge desc-attr + {:resource-type resource-type + :name name + :endpoint endpoint + :capabilities capabilities + :status (or status "ONLINE") + :created (time/now-str) + :updated (time/now-str)}) + (:description body) (assoc :description (:description body)) + (:mec-host-id body) (assoc :mec-host-id (:mec-host-id body)) + (:resources body) (assoc :resources (:resources body)) + (:credential-id body) (assoc :credential-id (:credential-id body)) + (:version body) (assoc :version (:version body)) + (:tags body) (assoc :tags (:tags body)))] + (add-impl (assoc request :body mepm-resource)))) + + +(def retrieve-impl (std-crud/retrieve-fn resource-type)) + +(defmethod crud/retrieve resource-type + [request] + (retrieve-impl request)) + + +(def edit-impl (std-crud/edit-fn resource-type)) + +(defmethod crud/edit resource-type + [request] + (edit-impl request)) + + +(def delete-impl (std-crud/delete-fn resource-type)) + +(defmethod crud/delete resource-type + [request] + (delete-impl request)) + + +(def query-impl (std-crud/query-fn resource-type collection-acl collection-type)) + +(defmethod crud/query resource-type + [request] + (query-impl request)) + + +;; +;; ACL +;; + +(defmethod crud/add-acl resource-type + [{:keys [acl] :as resource} request] + (if acl + resource + (a/add-acl resource request))) + + +;; +;; Actions +;; + +(defmethod crud/set-operations resource-type + [{:keys [id] :as resource} request] + (let [can-manage? (a/can-manage? resource request)] + (cond-> (crud/set-standard-operations resource request) + can-manage? (update :operations conj (u/action-map id :check-health)) + can-manage? (update :operations conj (u/action-map id :query-capabilities)) + can-manage? (update :operations conj (u/action-map id :query-resources))))) + + +;; +;; Check health action - query MEPM status via Mm5 +;; + +(defmethod crud/do-action [resource-type "check-health"] + [{{uuid :uuid} :params :as request}] + (try + (let [id (str resource-type "/" uuid) + mepm (crud/retrieve-by-id-as-admin id) + endpoint (:endpoint mepm) + current-time (time/now-str) + + ;; Perform actual Mm5 health check + health-result (mm3/check-health endpoint)] + + (if (:success? health-result) + (do + ;; Update last-check timestamp and status based on health check + (db/edit (assoc mepm + :last-check current-time + :status "ONLINE" + :updated current-time)) + (log/info "MEPM" id "health check successful") + (r/map-response {:message "MEPM health check completed" + :status "ONLINE" + :last-check current-time + :health-data (:data health-result)} + 200 id)) + (do + ;; Mark as degraded/offline if health check fails + (db/edit (assoc mepm + :last-check current-time + :status "DEGRADED" + :updated current-time)) + (log/warn "MEPM" id "health check failed:" (:message health-result)) + (r/map-response {:message (str "Health check failed: " (:message health-result)) + :status "DEGRADED" + :error (:error health-result)} + 503 id)))) + (catch Exception e + (log/error e "Failed to check MEPM health") + (r/map-response (str "Health check failed: " (.getMessage e)) 500)))) + + +;; +;; Query capabilities action - get MEPM capabilities via Mm5 +;; + +(defmethod crud/do-action [resource-type "query-capabilities"] + [{{uuid :uuid} :params :as request}] + (try + (let [id (str resource-type "/" uuid) + mepm (crud/retrieve-by-id-as-admin id) + endpoint (:endpoint mepm) + + ;; Perform actual Mm5 capabilities query + cap-result (mm3/query-capabilities endpoint)] + + (if (:success? cap-result) + (let [capabilities (:data cap-result)] + ;; Update stored capabilities with fresh data from MEPM + (db/edit (assoc mepm + :capabilities capabilities + :updated (time/now-str))) + (log/info "MEPM" id "capabilities queried successfully") + (r/map-response capabilities 200 id)) + (do + (log/warn "MEPM" id "capabilities query failed:" (:message cap-result)) + (r/map-response {:message (str "Capabilities query failed: " (:message cap-result)) + :error (:error cap-result) + :cached-capabilities (:capabilities mepm)} + 503 id)))) + (catch Exception e + (log/error e "Failed to query MEPM capabilities") + (r/map-response (str "Capabilities query failed: " (.getMessage e)) 500)))) + + +;; +;; Query resources action - get available resources via Mm5 +;; + +(defmethod crud/do-action [resource-type "query-resources"] + [{{uuid :uuid} :params :as request}] + (try + (let [id (str resource-type "/" uuid) + mepm (crud/retrieve-by-id-as-admin id) + endpoint (:endpoint mepm) + + ;; Perform actual Mm5 resources query + res-result (mm3/query-resources endpoint)] + + (if (:success? res-result) + (let [resources (:data res-result)] + ;; Update stored resources with fresh data from MEPM + (db/edit (assoc mepm + :resources resources + :updated (time/now-str))) + (log/info "MEPM" id "resources queried successfully") + (r/map-response resources 200 id)) + (do + (log/warn "MEPM" id "resources query failed:" (:message res-result)) + (r/map-response {:message (str "Resources query failed: " (:message res-result)) + :error (:error res-result) + :cached-resources (:resources mepm)} + 503 id)))) + (catch Exception e + (log/error e "Failed to query MEPM resources") + (r/map-response (str "Resources query failed: " (.getMessage e)) 500)))) diff --git a/code/src/com/sixsq/nuvla/server/resources/module.clj b/code/src/com/sixsq/nuvla/server/resources/module.clj index 8351a6380..73dba329e 100644 --- a/code/src/com/sixsq/nuvla/server/resources/module.clj +++ b/code/src/com/sixsq/nuvla/server/resources/module.clj @@ -18,6 +18,7 @@ component, or application. [com.sixsq.nuvla.server.resources.job.utils :as job-utils] [com.sixsq.nuvla.server.resources.module-application :as module-application] [com.sixsq.nuvla.server.resources.module-application-helm :as module-application-helm] + [com.sixsq.nuvla.server.resources.module-application-mec :as module-application-mec] [com.sixsq.nuvla.server.resources.module-applications-sets :as module-applications-sets] [com.sixsq.nuvla.server.resources.module-component :as module-component] [com.sixsq.nuvla.server.resources.module.utils :as utils] @@ -67,6 +68,7 @@ component, or application. (utils/is-component? resource) module-component/resource-type (utils/is-application? resource) module-application/resource-type (utils/is-application-helm? resource) module-application-helm/resource-type + (utils/is-application-mec? resource) module-application-mec/resource-type (utils/is-application-k8s? resource) module-application/resource-type (utils/is-applications-sets? resource) module-applications-sets/resource-type :else (throw (r/ex-bad-request (str "unknown module subtype: " diff --git a/code/src/com/sixsq/nuvla/server/resources/module/utils.clj b/code/src/com/sixsq/nuvla/server/resources/module/utils.clj index 7dc45aa63..f7f7fb49c 100644 --- a/code/src/com/sixsq/nuvla/server/resources/module/utils.clj +++ b/code/src/com/sixsq/nuvla/server/resources/module/utils.clj @@ -41,6 +41,10 @@ [resource] (is-subtype? resource module-spec/subtype-app-helm)) +(defn is-application-mec? + [resource] + (is-subtype? resource module-spec/subtype-app-mec)) + (defn is-applications-sets? [resource] (is-subtype? resource module-spec/subtype-apps-sets)) diff --git a/code/src/com/sixsq/nuvla/server/resources/module_application_mec.clj b/code/src/com/sixsq/nuvla/server/resources/module_application_mec.clj new file mode 100644 index 000000000..25123b5ac --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/module_application_mec.clj @@ -0,0 +1,344 @@ +(ns com.sixsq.nuvla.server.resources.module-application-mec + "MEC 037 Application Descriptor (AppD) module subtype implementation + + Standard: ETSI GS MEC 037 v3.2.1 + + This module type provides native MEC compliance for application descriptors, + implementing the Mm9 (Package Management) reference point with standard + ETSI MEC AppD format. + + Features: + - Full MEC 037 AppD schema validation + - Resource requirement specifications + - MEC service dependencies + - Traffic and DNS rule descriptors + - Multi-architecture container support + - Integration with MEC 010-2 lifecycle API" + (:require + [clojure.tools.logging :as log] + [com.sixsq.nuvla.server.resources.common.crud :as crud] + [com.sixsq.nuvla.server.resources.common.std-crud :as std-crud] + [com.sixsq.nuvla.server.resources.common.utils :as u] + [com.sixsq.nuvla.server.resources.resource-metadata :as md] + [com.sixsq.nuvla.server.resources.spec.module-application-mec :as spec-mec] + [com.sixsq.nuvla.server.util.metadata :as gen-md])) + + +;; +;; Constants +;; + +(def ^:const subtype "application_mec") + +(def ^:const resource-type (u/ns->type *ns*)) + + +(def ^:const collection-type (u/ns->collection-type *ns*)) + + +(def collection-acl {:query ["group/nuvla-admin"] + :add ["group/nuvla-admin"]}) + + +(def resource-acl {:owners ["group/nuvla-admin"]}) + + +;; +;; Validation Functions +;; + +(defn validate-appd-content + "Validates MEC AppD content structure against ETSI MEC 037 spec" + [content] + (when-not (spec-mec/valid-mec-appd? content) + (let [problems (spec-mec/mec-appd-problems content)] + (throw (ex-info "Invalid MEC AppD content" + {:status 400 + :problems problems + :explanation (with-out-str (spec-mec/explain-mec-appd content))})))) + content) + + +(defn validate-resource-requirements + "Validates that resource requirements are reasonable" + [content] + (let [compute (:virtualComputeDescriptor content) + cpu-count (get-in compute [:virtualCpu :numVirtualCpu]) + memory-mb (get-in compute [:virtualMemory :virtualMemSize])] + + ;; Warn if requirements are excessive + (when (> cpu-count 64) + (log/warn "MEC AppD requests excessive CPU:" cpu-count "cores")) + + (when (> memory-mb 262144) ;; 256GB + (log/warn "MEC AppD requests excessive memory:" memory-mb "MB")) + + ;; Check storage requirements + (doseq [storage (:virtualStorageDescriptor content)] + (let [size-gb (:sizeOfStorage storage)] + (when (> size-gb 5000) + (log/warn "MEC AppD requests excessive storage:" size-gb "GB"))))) + + content) + + +(defn validate-mec-services + "Validates MEC service dependencies" + [content] + (let [services (:appServiceRequired content)] + (doseq [service services] + (let [ser-name (:serName service) + version (:version service)] + (log/info "MEC AppD requires service:" ser-name "version:" version) + + ;; Could validate against supported MEC services + (when-not (contains? #{:rnis :location :ue-identity :bandwidth-management + :wlan-information :fixed-access-information + :traffic-management} + ser-name) + (log/warn "Unknown MEC service requested:" ser-name))))) + + content) + + +(defn validate-container-images + "Validates software image descriptors" + [content] + (let [images (:swImageDescriptor content)] + (when (empty? images) + (throw (ex-info "At least one software image is required" + {:status 400}))) + + (doseq [image images] + (let [sw-image (:swImage image) + container-format (:containerFormat image)] + + ;; Validate image reference format + (when-not (re-matches #"^[a-z0-9]+([\.\-][a-z0-9]+)*(/[a-z0-9]+([\.\-][a-z0-9]+)*)*:[a-zA-Z0-9\.\-_]+$" + sw-image) + (throw (ex-info "Invalid container image reference format" + {:status 400 + :swImage sw-image}))) + + ;; Currently only support Docker + (when-not (= :DOCKER container-format) + (log/warn "Non-Docker container format may not be supported:" container-format))))) + + content) + + +(defn validate-traffic-rules + "Validates traffic rule descriptors" + [content] + (let [rules (:trafficRuleDescriptor content)] + (doseq [rule rules] + (let [priority (:priority rule)] + (when-not (<= 0 priority 255) + (throw (ex-info "Traffic rule priority must be 0-255" + {:status 400 + :trafficRuleId (:trafficRuleId rule) + :priority priority})))))) + + content) + + +(defn validate-dns-rules + "Validates DNS rule descriptors" + [content] + (let [rules (:dnsRuleDescriptor content)] + (doseq [rule rules] + (let [domain (:domainName rule) + ip (:ipAddress rule) + ip-type (:ipAddressType rule)] + + ;; Validate IP address format matches type + (when (= :IPV4 ip-type) + (when-not (re-matches #"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$" ip) + (throw (ex-info "Invalid IPv4 address format" + {:status 400 + :dnsRuleId (:dnsRuleId rule) + :ipAddress ip})))) + + (when (= :IPV6 ip-type) + (when-not (re-matches #"^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$" ip) + (throw (ex-info "Invalid IPv6 address format" + {:status 400 + :dnsRuleId (:dnsRuleId rule) + :ipAddress ip}))))))) + + content) + + +;; +;; Multi-method dispatching +;; + +(def validate-fn (u/create-spec-validation-fn ::spec-mec/schema)) +(defmethod crud/validate resource-type + [resource] + (validate-fn resource)) + + +(defmethod crud/add-acl resource-type + [resource _request] + (assoc resource :acl resource-acl)) + + +;; +;; Resource Metadata Extraction +;; + +(defn extract-resource-summary + "Extracts resource requirements summary from MEC AppD" + [content] + (let [compute (:virtualComputeDescriptor content) + storage (:virtualStorageDescriptor content) + images (:swImageDescriptor content)] + {:cpus (get-in compute [:virtualCpu :numVirtualCpu]) + :memory-mb (get-in compute [:virtualMemory :virtualMemSize]) + :storage-gb (reduce + 0 (map :sizeOfStorage storage)) + :images (count images) + :mec-version (:mecVersion content) + :requires-mec-services (vec (map :serName (:appServiceRequired content)))})) + + +(defn extract-deployment-info + "Extracts deployment-relevant information from MEC AppD" + [content] + {:app-name (:appName content) + :app-version (:appSoftVersion content) + :provider (:appProvider content) + :description (:appDescription content) + :container-images (map #(select-keys % [:swImageName :swImageVersion :swImage :containerFormat]) + (:swImageDescriptor content)) + :resource-requirements (extract-resource-summary content) + :network-requirements {:external-connections (count (:appExtCpd content)) + :traffic-rules (count (:trafficRuleDescriptor content)) + :dns-rules (count (:dnsRuleDescriptor content))} + :mec-services (map #(select-keys % [:serName :version]) + (:appServiceRequired content))}) + + +;; +;; CRUD Operations +;; + +(def add-impl (std-crud/add-fn resource-type collection-acl resource-type)) + +(defmethod crud/add resource-type + [{{:keys [subtype content] :as body} :body :as request}] + (when-not (= subtype "application_mec") + (throw (ex-info "Invalid module subtype" + {:status 400 + :expected "application_mec" + :actual subtype}))) + + ;; Validate MEC AppD content + (-> content + validate-appd-content + validate-resource-requirements + validate-mec-services + validate-container-images + validate-traffic-rules + validate-dns-rules) + + (log/info "Creating MEC AppD module") + (let [response (add-impl request) + module-id (get-in response [:body :resource-id])] + + ;; Log deployment info for monitoring + (when module-id + (let [deploy-info (extract-deployment-info content)] + (log/info "MEC AppD module created:" + "id:" module-id + "app:" (:app-name deploy-info) + "version:" (:app-version deploy-info) + "cpus:" (get-in deploy-info [:resource-requirements :cpus]) + "memory:" (get-in deploy-info [:resource-requirements :memory-mb]) "MB" + "services:" (vec (map :serName (:mec-services deploy-info)))))) + + response)) + + +(def retrieve-impl (std-crud/retrieve-fn resource-type)) + + +(defmethod crud/retrieve resource-type + [request] + (retrieve-impl request)) + + +(def edit-impl (std-crud/edit-fn resource-type)) + + +(defmethod crud/edit resource-type + [request] + (edit-impl request)) + + +(def delete-impl (std-crud/delete-fn resource-type)) + + +(defmethod crud/delete resource-type + [request] + (delete-impl request)) + + +(def query-impl (std-crud/query-fn resource-type collection-acl collection-type)) + + +(defmethod crud/query resource-type + [request] + (query-impl request)) + + +;; +;; Helper Functions for Integration +;; + +(defn appd->deployment-params + "Converts MEC AppD to deployment parameters for MEPM via Mm3" + [module-id content] + (let [compute (:virtualComputeDescriptor content) + images (:swImageDescriptor content) + primary-image (first images)] + {:appDId module-id + :appName (:appName content) + :appProvider (:appProvider content) + :appSoftVersion (:appSoftVersion content) + :virtualComputeDescriptor compute + :swImageDescriptor images + :containerImage (:swImage primary-image) + :containerFormat (name (:containerFormat primary-image)) + :appServiceRequired (vec (map #(select-keys % [:serName :version]) + (:appServiceRequired content))) + :trafficRuleDescriptor (:trafficRuleDescriptor content) + :dnsRuleDescriptor (:dnsRuleDescriptor content)})) + + +(defn check-mec-compatibility + "Checks if MEC AppD is compatible with target MEPM capabilities" + [content mepm-capabilities] + (let [required-version (:mecVersion content) + mepm-version (:mecVersion mepm-capabilities) + required-services (set (map :serName (:appServiceRequired content))) + available-services (set (:availableServices mepm-capabilities))] + + {:compatible? (and (>= (compare mepm-version required-version) 0) + (clojure.set/subset? required-services available-services)) + :version-match? (>= (compare mepm-version required-version) 0) + :services-match? (clojure.set/subset? required-services available-services) + :missing-services (clojure.set/difference required-services available-services)})) + + +;; +;; Initialization +;; + +(def resource-metadata (gen-md/generate-metadata ::ns ::spec-mec/schema)) + +(defn initialize + [] + (log/info "Initializing MEC 037 AppD module subtype:" subtype) + (std-crud/initialize resource-type ::spec-mec/schema) + (md/register resource-metadata)) diff --git a/code/src/com/sixsq/nuvla/server/resources/spec/mepm.cljc b/code/src/com/sixsq/nuvla/server/resources/spec/mepm.cljc new file mode 100644 index 000000000..94a84ea56 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/spec/mepm.cljc @@ -0,0 +1,161 @@ +(ns com.sixsq.nuvla.server.resources.spec.mepm + "Schema for MEC Platform Manager (MEPM) resource. + + A MEPM represents an external MEC Platform Manager that Nuvla (MEO) + communicates with via the Mm5 interface. MEPMs manage host-level + platform operations on MEC hosts." + (:require + [clojure.spec.alpha :as s] + [com.sixsq.nuvla.server.resources.spec.common :as c] + [com.sixsq.nuvla.server.resources.spec.core :as cimi-core] + [com.sixsq.nuvla.server.util.spec :as su] + [spec-tools.core :as st])) + + +(s/def ::name + (-> (st/spec ::cimi-core/nonblank-string) + (assoc :name "name" + :json-schema/description "human-readable name of the MEPM" + :json-schema/order 20))) + + +(s/def ::description + (-> (st/spec ::cimi-core/nonblank-string) + (assoc :name "description" + :json-schema/description "description of the MEPM" + :json-schema/order 21))) + + +(s/def ::endpoint + (-> (st/spec ::cimi-core/nonblank-string) + (assoc :name "endpoint" + :json-schema/description "Mm5 interface endpoint URL (e.g., https://mepm.example.com/mm5)" + :json-schema/order 22))) + + +(s/def ::mec-host-id + (-> (st/spec ::cimi-core/nonblank-string) + (assoc :name "mec-host-id" + :json-schema/description "optional reference to associated NuvlaBox (MEC host)" + :json-schema/order 23))) + + +(s/def ::platforms + (-> (st/spec (s/coll-of ::cimi-core/nonblank-string :kind vector?)) + (assoc :name "platforms" + :json-schema/type "array" + :json-schema/description "supported container platforms (e.g., kubernetes, docker)" + :json-schema/order 24))) + + +(s/def ::services + (-> (st/spec (s/coll-of ::cimi-core/nonblank-string :kind vector?)) + (assoc :name "services" + :json-schema/type "array" + :json-schema/description "available MEC platform services (e.g., traffic-rules, dns-rules)" + :json-schema/order 25))) + + +(s/def ::api-version + (-> (st/spec ::cimi-core/nonblank-string) + (assoc :name "api-version" + :json-schema/description "MEC API version supported (e.g., 3.1.1)" + :json-schema/order 26))) + + +(s/def ::capabilities + (-> (st/spec (su/only-keys :req-un [::platforms] + :opt-un [::services ::api-version])) + (assoc :name "capabilities" + :json-schema/description "MEPM capabilities and supported features" + :json-schema/order 27))) + + +(s/def ::cpu-cores + (-> (st/spec pos-int?) + (assoc :name "cpu-cores" + :json-schema/type "integer" + :json-schema/description "number of CPU cores available" + :json-schema/order 28))) + + +(s/def ::memory-gb + (-> (st/spec pos-int?) + (assoc :name "memory-gb" + :json-schema/type "integer" + :json-schema/description "memory available in GB" + :json-schema/order 29))) + + +(s/def ::storage-gb + (-> (st/spec pos-int?) + (assoc :name "storage-gb" + :json-schema/type "integer" + :json-schema/description "storage available in GB" + :json-schema/order 30))) + + +(s/def ::gpu-count + (-> (st/spec nat-int?) + (assoc :name "gpu-count" + :json-schema/type "integer" + :json-schema/description "number of GPUs available" + :json-schema/order 31))) + + +(s/def ::resources + (-> (st/spec (su/only-keys :opt-un [::cpu-cores ::memory-gb ::storage-gb ::gpu-count])) + (assoc :name "resources" + :json-schema/description "available compute resources managed by MEPM" + :json-schema/order 32))) + + +(s/def ::status + (-> (st/spec #{"ONLINE" "OFFLINE" "DEGRADED" "ERROR"}) + (assoc :name "status" + :json-schema/type "string" + :json-schema/description "current status of the MEPM" + :json-schema/value-scope {:values ["ONLINE" "OFFLINE" "DEGRADED" "ERROR"] + :default "ONLINE"} + :json-schema/order 33))) + + +(s/def ::credential-id + (-> (st/spec ::cimi-core/nonblank-string) + (assoc :name "credential-id" + :json-schema/description "reference to credential resource for Mm5 authentication" + :json-schema/order 34))) + + +(s/def ::version + (-> (st/spec ::cimi-core/nonblank-string) + (assoc :name "version" + :json-schema/description "MEPM software version" + :json-schema/order 35))) + + +(s/def ::tags + (-> (st/spec (s/coll-of ::cimi-core/nonblank-string :kind vector?)) + (assoc :name "tags" + :json-schema/type "array" + :json-schema/description "tags for categorization (e.g., production, 5g, edge)" + :json-schema/order 36))) + + +(s/def ::last-check + (-> (st/spec ::cimi-core/timestamp) + (assoc :name "last-check" + :json-schema/description "timestamp of last health check" + :json-schema/order 37))) + + +(s/def ::schema + (su/only-keys-maps c/common-attrs + {:req-un [::name ::endpoint ::capabilities ::status] + :opt-un [::description + ::mec-host-id + ::resources + ::credential-id + ::version + ::tags + ::last-check]})) diff --git a/code/src/com/sixsq/nuvla/server/resources/spec/module.cljc b/code/src/com/sixsq/nuvla/server/resources/spec/module.cljc index 9a6f101b6..0e4fb838f 100644 --- a/code/src/com/sixsq/nuvla/server/resources/spec/module.cljc +++ b/code/src/com/sixsq/nuvla/server/resources/spec/module.cljc @@ -46,6 +46,7 @@ (def ^:const subtype-app-docker "Docker Application" "application") (def ^:const subtype-app-k8s "Kubernetes Application" "application_kubernetes") (def ^:const subtype-app-helm "Helm Application" "application_helm") +(def ^:const subtype-app-mec "MEC Application (ETSI MEC 037)" "application_mec") (def ^:const subtype-apps-sets "Application Bouquet" "applications_sets") (def ^:const module-subtypes @@ -54,6 +55,7 @@ subtype-app-docker subtype-app-k8s subtype-app-helm + subtype-app-mec subtype-apps-sets]) (def ^:const compatibility-docker-compose "docker-compose") diff --git a/code/src/com/sixsq/nuvla/server/resources/spec/module_application_mec.cljc b/code/src/com/sixsq/nuvla/server/resources/spec/module_application_mec.cljc new file mode 100644 index 000000000..e9d328491 --- /dev/null +++ b/code/src/com/sixsq/nuvla/server/resources/spec/module_application_mec.cljc @@ -0,0 +1,414 @@ +(ns com.sixsq.nuvla.server.resources.spec.module-application-mec + "Clojure spec for MEC 037 Application Descriptor (AppD) module subtype + + Standard: ETSI GS MEC 037 v3.2.1 + Purpose: Define MEC-native application descriptors for Nuvla module catalog + + This spec defines the structure for MEC applications following the ETSI MEC 037 + Application Descriptor format, enabling native MEC compliance for the Mm9 + (Package Management) reference point." + (:require + [clojure.spec.alpha :as s] + [com.sixsq.nuvla.server.resources.spec.common :as common] + [com.sixsq.nuvla.server.resources.spec.core :as core] + [com.sixsq.nuvla.server.util.spec :as su])) + + +;; +;; MEC 037 AppD Core Attributes +;; + +(s/def ::appDId + (s/and string? #(re-matches #"^module/[a-z0-9]+(-[a-z0-9]+)*$" %))) + +(s/def ::appDVersion + (s/and string? #(re-matches #"^\d+\.\d+(\.\d+)?$" %))) + +(s/def ::appName + (s/and string? #(<= 1 (count %) 100))) + +(s/def ::appProvider + (s/and string? #(<= 1 (count %) 100))) + +(s/def ::appSoftVersion + (s/and string? #(re-matches #"^\d+\.\d+(\.\d+)?$" %))) + +(s/def ::mecVersion + (s/and string? #(re-matches #"^\d+\.\d+\.\d+$" %))) + +(s/def ::appInfoName + (s/and string? #(<= 1 (count %) 200))) + +(s/def ::appDescription + (s/and string? #(<= 1 (count %) 1000))) + + +;; +;; Virtual Compute Descriptor +;; + +(s/def ::numVirtualCpu pos-int?) + +(s/def ::virtualCpuClock + (s/and number? pos?)) + +(s/def ::virtualCpuPinning + #{:STATIC :DYNAMIC}) + +(s/def ::virtualCpu + (s/keys :req-un [::numVirtualCpu] + :opt-un [::virtualCpuClock ::virtualCpuPinning])) + +(s/def ::virtualMemSize + (s/and pos-int? #(<= 256 % 524288))) ;; 256MB to 512GB in MB + +(s/def ::numaEnabled boolean?) + +(s/def ::virtualMemory + (s/keys :req-un [::virtualMemSize] + :opt-un [::numaEnabled])) + +(s/def ::computeId + (s/and string? #(<= 1 (count %) 100))) + +(s/def ::logicalNode + (s/and string? #(<= 1 (count %) 100))) + +(s/def ::virtualComputeDescriptor + (s/keys :req-un [::virtualCpu ::virtualMemory] + :opt-un [::computeId ::logicalNode])) + + +;; +;; Virtual Storage Descriptor +;; + +(s/def ::typeOfStorage + #{:BLOCK :OBJECT :FILE}) + +(s/def ::sizeOfStorage + (s/and pos-int? #(<= 1 % 10000))) ;; 1GB to 10TB in GB + +(s/def ::rdmaEnabled boolean?) + +(s/def ::id + (s/and string? #(<= 1 (count %) 100))) + +(s/def ::virtualStorageDescriptor-item + (s/keys :req-un [::typeOfStorage ::sizeOfStorage] + :opt-un [::rdmaEnabled ::id])) + +(s/def ::virtualStorageDescriptor + (s/coll-of ::virtualStorageDescriptor-item :kind vector?)) + + +;; +;; Software Image Descriptor +;; + +(s/def ::swImageName + (s/and string? #(<= 1 (count %) 200))) + +(s/def ::swImageVersion + (s/and string? #(re-matches #"^\d+\.\d+(\.\d+)?$" %))) + +(s/def ::containerFormat + #{:DOCKER :ACI :OCI}) + +(s/def ::swImage + (s/and string? #(re-matches #"^[a-z0-9]+([\.\-][a-z0-9]+)*(/[a-z0-9]+([\.\-][a-z0-9]+)*)*:[a-zA-Z0-9\.\-_]+$" %))) + +(s/def ::minDisk + (s/and pos-int? #(<= 1 % 1000))) ;; 1GB to 1TB in GB + +(s/def ::minRam + (s/and pos-int? #(<= 256 % 524288))) ;; 256MB to 512GB in MB + +(s/def ::diskFormat + #{:RAW :QCOW2 :VDI :VMDK :VHD}) + +(s/def ::operatingSystem + (s/and string? #(<= 1 (count %) 100))) + +(s/def ::supportedVirtualisationEnvironment + (s/coll-of string? :kind vector? :min-count 1)) + +(s/def ::swImageDescriptor-item + (s/keys :req-un [::swImageName ::swImageVersion ::containerFormat ::swImage] + :opt-un [::minDisk ::minRam ::diskFormat ::operatingSystem + ::supportedVirtualisationEnvironment])) + +(s/def ::swImageDescriptor + (s/coll-of ::swImageDescriptor-item :kind vector? :min-count 1)) + + +;; +;; External Connection Point Descriptor +;; + +(s/def ::cpdId + (s/and string? #(re-matches #"^[a-z0-9\-]+$" %))) + +(s/def ::layerProtocol + #{:TCP :UDP :HTTP :HTTPS :WEBSOCKET}) + +(s/def ::addressType + #{:IPV4 :IPV6 :MAC}) + +(s/def ::iPAddressAssignment + #{:DYNAMIC :STATIC}) + +(s/def ::floatingIpActivated boolean?) + +(s/def ::numberOfIpAddress pos-int?) + +(s/def ::logicalNodeRequirements + (s/keys :opt-un [::logicalNode])) + +(s/def ::l3AddressData + (s/keys :req-un [::addressType ::iPAddressAssignment] + :opt-un [::floatingIpActivated ::numberOfIpAddress])) + +(s/def ::nicIoRequirements + (s/keys :opt-un [::logicalNodeRequirements])) + +(s/def ::bandwidthRequirements + (s/and pos-int? #(<= 1 % 100000))) ;; Mbps + +(s/def ::virtualNetworkInterfaceRequirements + (s/keys :opt-un [::nicIoRequirements ::bandwidthRequirements])) + +(s/def ::appExtCpd-item + (s/keys :req-un [::cpdId ::layerProtocol] + :opt-un [::l3AddressData ::virtualNetworkInterfaceRequirements])) + +(s/def ::appExtCpd + (s/coll-of ::appExtCpd-item :kind vector?)) + + +;; +;; MEC Service Requirements +;; + +(s/def ::serName + #{:rnis :location :ue-identity :bandwidth-management :wlan-information + :fixed-access-information :traffic-management}) + +(s/def ::serCategory + (s/and string? #(re-matches #"^[a-z0-9\-]+$" %))) + +(s/def ::version + (s/and string? #(re-matches #"^\d+\.\d+\.\d+$" %))) + +(s/def ::transportDependencyType + #{:REST :WEBSOCKET :MQTT :AMQP}) + +(s/def ::serializer + #{:JSON :XML :PROTOBUF}) + +(s/def ::labels + (s/map-of keyword? string?)) + +(s/def ::transportDependency + (s/keys :req-un [::transportDependencyType] + :opt-un [::serializer ::labels])) + +(s/def ::requestedPermissions + (s/coll-of keyword? :kind vector? :min-count 1)) + +(s/def ::appServiceRequired-item + (s/keys :req-un [::serName] + :opt-un [::serCategory ::version ::transportDependency + ::requestedPermissions])) + +(s/def ::appServiceRequired + (s/coll-of ::appServiceRequired-item :kind vector?)) + + +;; +;; Traffic Rule Descriptor +;; + +(s/def ::trafficRuleId + (s/and string? #(re-matches #"^[a-z0-9\-]+$" %))) + +(s/def ::filterType + #{:FLOW :PACKET :HTTP}) + +(s/def ::priority + (s/and int? #(<= 0 % 255))) + +(s/def ::srcAddress + (s/coll-of string? :kind vector?)) + +(s/def ::dstAddress + (s/coll-of string? :kind vector?)) + +(s/def ::srcPort + (s/coll-of string? :kind vector?)) + +(s/def ::dstPort + (s/coll-of string? :kind vector?)) + +(s/def ::protocol + (s/coll-of string? :kind vector?)) + +(s/def ::trafficFilter + (s/keys :req-un [::filterType] + :opt-un [::srcAddress ::dstAddress ::srcPort ::dstPort ::protocol])) + +(s/def ::action + #{:DROP :FORWARD :PASSTHROUGH :DUPLICATE}) + +(s/def ::interfaceType + #{:TUNNEL :MAC :IP}) + +(s/def ::state + #{:ACTIVE :INACTIVE}) + +(s/def ::dstInterface + (s/coll-of (s/keys :req-un [::interfaceType]) :kind vector?)) + +(s/def ::trafficRuleDescriptor-item + (s/keys :req-un [::trafficRuleId ::filterType ::priority ::trafficFilter ::action] + :opt-un [::dstInterface ::state])) + +(s/def ::trafficRuleDescriptor + (s/coll-of ::trafficRuleDescriptor-item :kind vector?)) + + +;; +;; DNS Rule Descriptor +;; + +(s/def ::dnsRuleId + (s/and string? #(re-matches #"^[a-z0-9\-]+$" %))) + +(s/def ::domainName + (s/and string? #(re-matches #"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$" %))) + +(s/def ::ipAddressType + #{:IPV4 :IPV6}) + +(s/def ::ipAddress + (s/and string? #(or (re-matches #"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$" %) + (re-matches #"^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$" %)))) + +(s/def ::ttl pos-int?) + +(s/def ::dnsRuleDescriptor-item + (s/keys :req-un [::dnsRuleId ::domainName ::ipAddressType ::ipAddress] + :opt-un [::ttl])) + +(s/def ::dnsRuleDescriptor + (s/coll-of ::dnsRuleDescriptor-item :kind vector?)) + + +;; +;; Feature Dependencies +;; + +(s/def ::featureName + (s/and string? #(<= 1 (count %) 100))) + +(s/def ::featureVersion + (s/and string? #(re-matches #"^\d+\.\d+(\.\d+)?$" %))) + +(s/def ::appFeatureRequired-item + (s/keys :req-un [::featureName ::featureVersion])) + +(s/def ::appFeatureRequired + (s/coll-of ::appFeatureRequired-item :kind vector?)) + + +;; +;; Latency Requirements +;; + +(s/def ::maxLatency pos-int?) ;; in milliseconds + +(s/def ::latencyDescriptor + (s/keys :req-un [::maxLatency])) + + +;; +;; Operation Configuration (optional, simplified) +;; + +(s/def ::terminateAppInstanceOpConfig + map?) + +(s/def ::changeAppInstanceStateOpConfig + map?) + + +;; +;; Complete MEC AppD Content Schema +;; + +(s/def ::mec-appd-content + (s/keys :req-un [::appDId + ::appDVersion + ::appName + ::appProvider + ::appSoftVersion + ::mecVersion + ::virtualComputeDescriptor + ::swImageDescriptor] + :opt-un [::appInfoName + ::appDescription + ::virtualStorageDescriptor + ::appExtCpd + ::appServiceRequired + ::appFeatureRequired + ::trafficRuleDescriptor + ::dnsRuleDescriptor + ::latencyDescriptor + ::terminateAppInstanceOpConfig + ::changeAppInstanceStateOpConfig])) + + +;; +;; Module Subtype Schema +;; + +(def subtype "application_mec") + +(s/def ::subtype #{subtype}) + +(s/def ::content ::mec-appd-content) + +(def module-application-mec-keys-spec + {:req-un [::subtype ::content] + :opt-un []}) + +(def module-application-mec-keys-href-opt-spec + (update-in module-application-mec-keys-spec [:opt-un] conj :com.sixsq.nuvla.server.resources.spec.module/href)) + +(s/def ::module-application-mec + (s/merge ::core/resource + (s/keys :req-un [::subtype ::content]))) + +(def module-application-mec-schema (su/only-keys-maps module-application-mec-keys-spec)) + +(s/def ::schema module-application-mec-schema) + + +;; +;; Validation Helpers +;; + +(defn valid-mec-appd? + "Validates a MEC AppD content structure" + [appd-content] + (s/valid? ::mec-appd-content appd-content)) + +(defn explain-mec-appd + "Explains validation errors for MEC AppD content" + [appd-content] + (s/explain ::mec-appd-content appd-content)) + +(defn mec-appd-problems + "Returns validation problems for MEC AppD content" + [appd-content] + (s/explain-data ::mec-appd-content appd-content)) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_op_tracking_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_op_tracking_test.clj new file mode 100644 index 000000000..84577b58c --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_op_tracking_test.clj @@ -0,0 +1,373 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-lcm-op-tracking-test + "Tests for MEC 010-2 Application Lifecycle Management Operation Tracking" + (:require + [clojure.test :refer [deftest is testing]] + [com.sixsq.nuvla.server.resources.mec.app-lcm-op-tracking :as tracking] + [com.sixsq.nuvla.server.util.time :as time-utils])) + + +;; +;; Test Fixtures +;; + +(def sample-app-instance-id "deployment/test-app-123") +(def sample-user-id "user/test-user") + +(def sample-instantiate-params + {:grantId "grant/123" + :flavourId "default"}) + +(def sample-terminate-params + {:terminationType "GRACEFUL"}) + +(def sample-operate-params + {:changeStateTo "STARTED"}) + + +;; +;; Job Creation Tests +;; + +(deftest test-create-operation-job + (testing "Create instantiate operation job" + (let [job (tracking/create-operation-job + :INSTANTIATE + sample-app-instance-id + sample-instantiate-params + sample-user-id)] + + (is (string? (:id job))) + (is (.startsWith (:id job) "job/")) + (is (= "job" (:resource-type job))) + (is (= "INSTANTIATE" (:action job))) + (is (= sample-app-instance-id (:target-resource job))) + (is (= "QUEUED" (:state job))) + (is (= 0 (:progress job))) + (is (= "INSTANTIATE" (:mec-operation-type job))) + (is (= sample-app-instance-id (:mec-app-instance-id job))) + (is (= sample-instantiate-params (:mec-request-params job))) + (is (some? (:start-time job))) + (is (some? (:state-entered-time job))))) + + (testing "Create terminate operation job" + (let [job (tracking/create-operation-job + :TERMINATE + sample-app-instance-id + sample-terminate-params + sample-user-id)] + + (is (= "TERMINATE" (:action job))) + (is (= "TERMINATE" (:mec-operation-type job))) + (is (= sample-terminate-params (:mec-request-params job))))) + + (testing "Create operate operation job" + (let [job (tracking/create-operation-job + :OPERATE + sample-app-instance-id + sample-operate-params + sample-user-id)] + + (is (= "OPERATE" (:action job))) + (is (= "OPERATE" (:mec-operation-type job))) + (is (= sample-operate-params (:mec-request-params job))))) + + (testing "Job ACL is set correctly" + (let [job (tracking/create-operation-job + :INSTANTIATE + sample-app-instance-id + sample-instantiate-params + sample-user-id)] + + (is (= [sample-user-id] (get-in job [:acl :owners]))) + (is (= [sample-user-id] (get-in job [:acl :view-data])))))) + + +(deftest test-start-operation-job + (testing "Start operation job transitions to RUNNING" + (let [job-id "job/test-123" + updated-job (tracking/start-operation-job job-id)] + + (is (= job-id (:id updated-job))) + (is (= "RUNNING" (:state updated-job))) + (is (= 10 (:progress updated-job))) + (is (= "Operation in progress" (:status-message updated-job))) + (is (some? (:updated updated-job))) + (is (some? (:state-entered-time updated-job)))))) + + +(deftest test-complete-operation-job + (testing "Complete operation job with result" + (let [job-id "job/test-123" + result {:app-instance-id "deployment/test-app-123" + :status "INSTANTIATED"} + completed-job (tracking/complete-operation-job job-id result)] + + (is (= job-id (:id completed-job))) + (is (= "SUCCESS" (:state completed-job))) + (is (= 100 (:progress completed-job))) + (is (= "Operation completed successfully" (:status-message completed-job))) + (is (= 0 (:return-code completed-job))) + (is (= result (:result completed-job))) + (is (some? (:updated completed-job))) + (is (some? (:state-entered-time completed-job))) + (is (some? (:time-of-status-change completed-job)))))) + + +(deftest test-fail-operation-job + (testing "Fail operation job with error detail" + (let [job-id "job/test-123" + error-detail {:type "about:blank" + :title "MEPM Connection Failed" + :status 503 + :detail "Failed to connect to MEPM endpoint"} + failed-job (tracking/fail-operation-job job-id error-detail)] + + (is (= job-id (:id failed-job))) + (is (= "FAILED" (:state failed-job))) + (is (= 0 (:progress failed-job))) + (is (= (:detail error-detail) (:status-message failed-job))) + (is (= 1 (:return-code failed-job))) + (is (= error-detail (:error failed-job))) + (is (some? (:updated failed-job))) + (is (some? (:state-entered-time failed-job))) + (is (some? (:time-of-status-change failed-job)))))) + + +;; +;; State Synchronization Tests +;; + +(deftest test-job-to-app-lcm-op-occ-conversion + (testing "Convert job to AppLcmOpOcc" + (let [job {:id "job/instantiate-123" + :state "RUNNING" + :operation-type "INSTANTIATE" + :target-resource "deployment/test-123" + :start-time "2025-10-21T10:00:00Z" + :state-entered-time "2025-10-21T10:00:05Z"} + op-occ (tracking/job->app-lcm-op-occ job)] + + (is (some? op-occ)) + (is (= "job/instantiate-123" (:lcmOpOccId op-occ))) + (is (= "INSTANTIATE" (:operationType op-occ))) + (is (= "PROCESSING" (:operationState op-occ))) + (is (= "deployment/test-123" (:appInstanceId op-occ))) + (is (= "2025-10-21T10:00:00Z" (:startTime op-occ))))) + + (testing "Convert completed job to AppLcmOpOcc" + (let [job {:id "job/instantiate-123" + :state "SUCCESS" + :operation-type "INSTANTIATE" + :target-resource "deployment/test-123" + :start-time "2025-10-21T10:00:00Z" + :state-entered-time "2025-10-21T10:00:05Z" + :time-of-status-change "2025-10-21T10:02:00Z"} + op-occ (tracking/job->app-lcm-op-occ job)] + + (is (= "COMPLETED" (:operationState op-occ))))) + + (testing "Convert failed job to AppLcmOpOcc with error" + (let [job {:id "job/instantiate-123" + :state "FAILED" + :operation-type "INSTANTIATE" + :target-resource "deployment/test-123" + :start-time "2025-10-21T10:00:00Z" + :state-entered-time "2025-10-21T10:00:05Z" + :status-message "MEPM connection failed" + :error {:type "about:blank" + :title "Connection Error" + :status 503 + :detail "MEPM connection failed"}} + op-occ (tracking/job->app-lcm-op-occ job)] + + (is (= "FAILED" (:operationState op-occ))) + (is (some? (:error op-occ))) + (is (= "MEPM connection failed" (get-in op-occ [:error :detail])))))) + + +(deftest test-get-operation-state + (testing "Get operation state for existing job" + (let [job-id "job/test-123" + op-occ (tracking/get-operation-state job-id)] + + (is (some? op-occ)) + (is (= job-id (:lcmOpOccId op-occ))) + (is (some? (:operationType op-occ))) + (is (some? (:operationState op-occ))))) + + (testing "Get operation state for non-existent job" + (let [op-occ (tracking/get-operation-state nil)] + (is (nil? op-occ))))) + + +;; +;; Operation History Tests +;; + +(deftest test-query-operations + (testing "Query operations with no filters" + (let [operations (tracking/query-operations {} {})] + (is (vector? operations)))) + + (testing "Query operations with app-instance-id filter" + (let [operations (tracking/query-operations + {:app-instance-id sample-app-instance-id} + {})] + (is (vector? operations)))) + + (testing "Query operations with operation-type filter" + (let [operations (tracking/query-operations + {:operation-type "INSTANTIATE"} + {})] + (is (vector? operations)))) + + (testing "Query operations with pagination" + (let [operations (tracking/query-operations + {} + {:limit 10 :offset 0})] + (is (vector? operations)))) + + (testing "Query operations with sorting" + (let [operations (tracking/query-operations + {} + {:sort-by :start-time :sort-order :desc})] + (is (vector? operations))))) + + +(deftest test-get-operation-history + (testing "Get operation history for app instance" + (let [history (tracking/get-operation-history sample-app-instance-id {})] + + (is (vector? history)))) + + (testing "Get operation history with limit" + (let [history (tracking/get-operation-history + sample-app-instance-id + {:limit 5})] + + (is (vector? history))))) + + +(deftest test-get-operation-by-id + (testing "Get operation occurrence by ID" + (let [op-occ-id "job/test-123" + op-occ (tracking/get-operation-by-id op-occ-id)] + + (is (some? op-occ)) + (is (= op-occ-id (:lcmOpOccId op-occ)))))) + + +;; +;; Operation Statistics Tests +;; + +(deftest test-get-operation-stats + (testing "Get operation statistics for app instance" + (let [stats (tracking/get-operation-stats sample-app-instance-id)] + + (is (map? stats)) + (is (contains? stats :total)) + (is (contains? stats :by-type)) + (is (contains? stats :by-state)) + (is (contains? stats :success-rate)) + (is (contains? stats :avg-duration)) + (is (number? (:total stats))) + (is (map? (:by-type stats))) + (is (map? (:by-state stats))) + (is (number? (:success-rate stats))) + (is (>= (:success-rate stats) 0.0)) + (is (<= (:success-rate stats) 100.0))))) + + +;; +;; Integration Tests +;; + +(deftest test-wrap-with-job-tracking-success + (testing "Wrap successful operation with job tracking" + (let [operation-fn (fn [params] + {:status :SUCCESS + :app-instance-id sample-app-instance-id + :result-data {:state "INSTANTIATED"}}) + result (tracking/wrap-with-job-tracking + operation-fn + :INSTANTIATE + sample-app-instance-id + sample-instantiate-params + sample-user-id)] + + (is (some? (:job-id result))) + (is (.startsWith (:job-id result) "job/")) + (is (= :SUCCESS (get-in result [:operation-result :status]))) + (is (some? (:app-lcm-op-occ result))) + (is (= "COMPLETED" (get-in result [:app-lcm-op-occ :operationState])))))) + + +(deftest test-wrap-with-job-tracking-failure + (testing "Wrap failed operation with job tracking" + (let [error-detail {:type "about:blank" + :title "MEPM Error" + :status 503 + :detail "MEPM unavailable"} + operation-fn (fn [params] + {:status :FAILED + :error-detail error-detail}) + result (tracking/wrap-with-job-tracking + operation-fn + :INSTANTIATE + sample-app-instance-id + sample-instantiate-params + sample-user-id)] + + (is (some? (:job-id result))) + (is (= :FAILED (get-in result [:operation-result :status]))) + (is (some? (:app-lcm-op-occ result))) + (is (= "FAILED" (get-in result [:app-lcm-op-occ :operationState]))) + (is (some? (get-in result [:app-lcm-op-occ :error]))))) + + +(deftest test-wrap-with-job-tracking-exception + (testing "Wrap operation that throws exception" + (let [operation-fn (fn [params] + (throw (Exception. "Unexpected error"))) + result (tracking/wrap-with-job-tracking + operation-fn + :INSTANTIATE + sample-app-instance-id + sample-instantiate-params + sample-user-id)] + + (is (some? (:job-id result))) + (is (= :FAILED (get-in result [:operation-result :status]))) + (is (some? (:app-lcm-op-occ result))) + (is (= "FAILED" (get-in result [:app-lcm-op-occ :operationState]))) + (is (= "Unexpected error" (get-in result [:app-lcm-op-occ :error :detail])))))) + + +;; +;; Test Summary +;; + +(deftest test-operation-tracking-complete + (testing "Operation tracking module is complete" + (is (fn? tracking/create-operation-job) + "Job creation function exists") + (is (fn? tracking/start-operation-job) + "Job start function exists") + (is (fn? tracking/complete-operation-job) + "Job completion function exists") + (is (fn? tracking/fail-operation-job) + "Job failure function exists") + (is (fn? tracking/job->app-lcm-op-occ) + "Job to AppLcmOpOcc conversion exists") + (is (fn? tracking/query-operations) + "Operation query function exists") + (is (fn? tracking/get-operation-history) + "Operation history function exists") + (is (fn? tracking/get-operation-by-id) + "Get operation by ID function exists") + (is (fn? tracking/get-operation-stats) + "Operation statistics function exists") + (is (fn? tracking/wrap-with-job-tracking) + "Job tracking wrapper exists"))) +) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_subscription_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_subscription_test.clj new file mode 100644 index 000000000..3a59108e6 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_subscription_test.clj @@ -0,0 +1,433 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-lcm-subscription-test + "Tests for MEC 010-2 Application Lifecycle Subscription" + (:require + [clojure.test :refer [deftest is testing]] + [com.sixsq.nuvla.server.resources.mec.app-lcm-subscription :as subscription])) + + +;; +;; Test Data +;; + +(def test-user-id "user/test-user") + +(def test-app-instance + {:id "deployment/abc-123" + :app-name "test-app" + :app-d-id "appd/test-1" + :instantiation-state "INSTANTIATED" + :operational-state "STARTED"}) + +(def test-app-lcm-op-occ + {:id "job/op-123" + :app-instance-id "deployment/abc-123" + :operation-type "INSTANTIATE" + :operation-state "COMPLETED" + :start-time (java.time.Instant/parse "2025-01-01T10:00:00Z") + :state-entered-time (java.time.Instant/parse "2025-01-01T10:05:00Z")}) + + +;; +;; Subscription Creation Tests +;; + +(deftest test-create-subscription-app-instance + (testing "Create AppInstanceStateChangeNotification subscription" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {:app-name "test-app" + :operational-state "STARTED"} + test-user-id)] + + (is (string? (:id sub))) + (is (.startsWith (:id sub) "subscription/")) + (is (= "AppInstanceStateChangeNotification" (:subscription-type sub))) + (is (= "https://example.com/webhook" (:callback-uri sub))) + (is (= {:app-name "test-app" + :operational-state "STARTED"} + (:app-instance-filter sub))) + (is (= test-user-id (:owner sub))) + (is (true? (:active sub))) + (is (some? (:created sub))) + (is (some? (:updated sub)))))) + + +(deftest test-create-subscription-app-lcm-op-occ + (testing "Create AppLcmOpOccStateChangeNotification subscription" + (let [sub (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "https://example.com/webhook" + {:operation-type "INSTANTIATE" + :operation-state "COMPLETED"} + test-user-id)] + + (is (string? (:id sub))) + (is (= "AppLcmOpOccStateChangeNotification" (:subscription-type sub))) + (is (= {:operation-type "INSTANTIATE" + :operation-state "COMPLETED"} + (:app-lcm-op-occ-filter sub))) + (is (nil? (:app-instance-filter sub)))))) + + +(deftest test-create-subscription-no-filter + (testing "Create subscription without filter (match all)" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {} + test-user-id)] + + (is (nil? (:app-instance-filter sub))) + (is (= "AppInstanceStateChangeNotification" (:subscription-type sub)))))) + + +;; +;; Validation Tests +;; + +(deftest test-validate-subscription-valid + (testing "Validate valid subscription" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {} + test-user-id) + result (subscription/validate-subscription sub)] + + (is (true? (:valid? result))) + (is (nil? (:errors result)))))) + + +(deftest test-validate-subscription-invalid-callback-uri + (testing "Validate subscription with invalid callback URI" + (let [sub {:id "subscription/test" + :subscription-type "AppInstanceStateChangeNotification" + :callback-uri "not-a-uri"} + result (subscription/validate-subscription sub)] + + (is (false? (:valid? result))) + (is (some? (:errors result)))))) + + +;; +;; Update Tests +;; + +(deftest test-update-subscription + (testing "Update subscription callback URI and filter" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {:app-name "test-app"} + test-user-id) + updated-sub (subscription/update-subscription + sub + {:callback-uri "https://example.com/new-webhook" + :app-instance-filter {:app-name "new-app"} + :active false})] + + (is (= "https://example.com/new-webhook" (:callback-uri updated-sub))) + (is (= {:app-name "new-app"} (:app-instance-filter updated-sub))) + (is (false? (:active updated-sub))) + (is (= (:id sub) (:id updated-sub))) + (is (= (:created sub) (:created updated-sub))) + (is (not= (:updated sub) (:updated updated-sub)))))) + + +(deftest test-deactivate-subscription + (testing "Deactivate subscription (soft delete)" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {} + test-user-id) + deactivated (subscription/deactivate-subscription sub)] + + (is (false? (:active deactivated))) + (is (not= (:updated sub) (:updated deactivated)))))) + + +;; +;; Filter Matching Tests +;; + +(deftest test-matches-app-instance-filter-no-filter + (testing "Empty filter matches all app instances" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {} + test-user-id)] + + (is (true? (subscription/matches-app-instance-filter? sub test-app-instance)))))) + + +(deftest test-matches-app-instance-filter-exact-match + (testing "Filter matches app instance exactly" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {:app-name "test-app" + :operational-state "STARTED"} + test-user-id)] + + (is (true? (subscription/matches-app-instance-filter? sub test-app-instance)))))) + + +(deftest test-matches-app-instance-filter-no-match + (testing "Filter does not match app instance" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {:app-name "different-app"} + test-user-id)] + + (is (false? (subscription/matches-app-instance-filter? sub test-app-instance)))))) + + +(deftest test-matches-app-instance-filter-partial-match + (testing "Partial filter matches (only some fields specified)" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {:operational-state "STARTED"} + test-user-id)] + + (is (true? (subscription/matches-app-instance-filter? sub test-app-instance)))))) + + +(deftest test-matches-app-lcm-op-occ-filter-match + (testing "Filter matches operation occurrence" + (let [sub (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "https://example.com/webhook" + {:operation-type "INSTANTIATE" + :operation-state "COMPLETED"} + test-user-id)] + + (is (true? (subscription/matches-app-lcm-op-occ-filter? sub test-app-lcm-op-occ)))))) + + +(deftest test-matches-app-lcm-op-occ-filter-no-match + (testing "Filter does not match operation occurrence" + (let [sub (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "https://example.com/webhook" + {:operation-type "TERMINATE"} + test-user-id)] + + (is (false? (subscription/matches-app-lcm-op-occ-filter? sub test-app-lcm-op-occ)))))) + + +;; +;; Notification Building Tests +;; + +(deftest test-build-app-instance-notification + (testing "Build AppInstanceStateChangeNotification" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {} + test-user-id) + notification (subscription/build-app-instance-notification + sub + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + (is (= "AppInstanceStateChangeNotification" (:notification-type notification))) + (is (string? (:notification-id notification))) + (is (.startsWith (:notification-id notification) "notification/")) + (is (= (:id sub) (:subscription-id notification))) + (is (= "deployment/abc-123" (:app-instance-id notification))) + (is (= "test-app" (:app-name notification))) + (is (= "STARTED" (:operational-state notification))) + (is (= "OPERATIONAL_STATE" (:change-type notification))) + (is (= "STOPPED" (:previous-state notification))) + (is (some? (:timestamp notification))) + (is (map? (:_links notification))) + (is (some? (get-in notification [:_links :subscription :href]))) + (is (some? (get-in notification [:_links :app-instance :href])))))) + + +(deftest test-build-app-lcm-op-occ-notification + (testing "Build AppLcmOpOccStateChangeNotification" + (let [sub (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "https://example.com/webhook" + {} + test-user-id) + notification (subscription/build-app-lcm-op-occ-notification + sub + test-app-lcm-op-occ + "OPERATION_STATE" + "PROCESSING")] + + (is (= "AppLcmOpOccStateChangeNotification" (:notification-type notification))) + (is (string? (:notification-id notification))) + (is (= (:id sub) (:subscription-id notification))) + (is (= "job/op-123" (:app-lcm-op-occ-id notification))) + (is (= "deployment/abc-123" (:app-instance-id notification))) + (is (= "INSTANTIATE" (:operation-type notification))) + (is (= "COMPLETED" (:operation-state notification))) + (is (= "OPERATION_STATE" (:change-type notification))) + (is (= "PROCESSING" (:previous-state notification))) + (is (= (java.time.Instant/parse "2025-01-01T10:00:00Z") (:start-time notification))) + (is (some? (:timestamp notification))) + (is (map? (:_links notification))) + (is (some? (get-in notification [:_links :subscription :href]))) + (is (some? (get-in notification [:_links :app-lcm-op-occ :href]))) + (is (some? (get-in notification [:_links :app-instance :href])))))) + + +;; +;; Query Tests +;; + +(deftest test-query-subscriptions-no-filter + (testing "Query all subscriptions" + (let [sub1 (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook1" + {} + test-user-id) + sub2 (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "https://example.com/webhook2" + {} + test-user-id) + subs [sub1 sub2] + result (subscription/query-subscriptions subs {})] + + (is (= 2 (count result))) + (is (some #(= (:id %) (:id sub1)) result)) + (is (some #(= (:id %) (:id sub2)) result))))) + + +(deftest test-query-subscriptions-by-type + (testing "Query subscriptions by type" + (let [sub1 (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook1" + {} + test-user-id) + sub2 (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "https://example.com/webhook2" + {} + test-user-id) + subs [sub1 sub2] + result (subscription/query-subscriptions + subs + {:subscription-type "AppInstanceStateChangeNotification"})] + + (is (= 1 (count result))) + (is (= (:id sub1) (:id (first result))))))) + + +(deftest test-query-subscriptions-by-owner + (testing "Query subscriptions by owner" + (let [sub1 (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook1" + {} + "user/owner1") + sub2 (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook2" + {} + "user/owner2") + subs [sub1 sub2] + result (subscription/query-subscriptions subs {:owner "user/owner1"})] + + (is (= 1 (count result))) + (is (= (:id sub1) (:id (first result))))))) + + +(deftest test-query-subscriptions-pagination + (testing "Query subscriptions with pagination" + (let [subs (mapv #(subscription/create-subscription + "AppInstanceStateChangeNotification" + (str "https://example.com/webhook" %) + {} + test-user-id) + (range 10)) + result1 (subscription/query-subscriptions subs {:limit 5 :offset 0}) + result2 (subscription/query-subscriptions subs {:limit 5 :offset 5})] + + (is (= 5 (count result1))) + (is (= 5 (count result2))) + (is (not= (map :id result1) (map :id result2)))))) + + +(deftest test-get-subscription-by-id + (testing "Get subscription by ID" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook" + {} + test-user-id) + subs [sub] + result (subscription/get-subscription-by-id subs (:id sub))] + + (is (= (:id sub) (:id result))) + (is (= (:callback-uri sub) (:callback-uri result)))))) + + +(deftest test-get-subscription-by-id-not-found + (testing "Get subscription by ID - not found" + (let [subs [] + result (subscription/get-subscription-by-id subs "subscription/nonexistent")] + + (is (nil? result))))) + + +(deftest test-get-active-subscriptions-for-type + (testing "Get active subscriptions for specific type" + (let [sub1 (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook1" + {} + test-user-id) + sub2 (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "https://example.com/webhook2" + {} + test-user-id) + sub3 (subscription/deactivate-subscription + (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://example.com/webhook3" + {} + test-user-id)) + subs [sub1 sub2 sub3] + result (subscription/get-active-subscriptions-for-type + subs + "AppInstanceStateChangeNotification")] + + (is (= 1 (count result))) + (is (= (:id sub1) (:id (first result))))))) + + +;; +;; Module Completeness Test +;; + +(deftest test-subscription-module-complete + (testing "Verify all expected functions are available" + (let [expected-fns ['create-subscription + 'validate-subscription + 'update-subscription + 'deactivate-subscription + 'matches-app-instance-filter? + 'matches-app-lcm-op-occ-filter? + 'build-app-instance-notification + 'build-app-lcm-op-occ-notification + 'query-subscriptions + 'get-subscription-by-id + 'get-active-subscriptions-for-type]] + (doseq [fn-name expected-fns] + (is (some? (ns-resolve 'com.sixsq.nuvla.server.resources.mec.app-lcm-subscription fn-name)) + (str "Function " fn-name " should be defined")))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_v2_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_v2_test.clj new file mode 100644 index 000000000..b2659ba6b --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/app_lcm_v2_test.clj @@ -0,0 +1,296 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-lcm-v2-test + "Tests for MEC 010-2 Application Lifecycle Management API + + Tests cover: + - Schema validation and state mapping + - CRUD operations for app instances + - Lifecycle operations (instantiate, terminate, operate) + - Operation occurrence tracking + - Error handling (RFC 7807) + + Standard: ETSI GS MEC 010-2 v2.2.1" + (:require + [clojure.test :refer [deftest is testing use-fixtures]] + [com.sixsq.nuvla.server.resources.mec.app-instance :as app-instance] + [com.sixsq.nuvla.server.resources.mec.app-lcm-op-occ :as app-lcm-op-occ] + [com.sixsq.nuvla.server.resources.mec.app-lcm-v2 :as app-lcm-v2])) + + +;; +;; Test Fixtures +;; + +(def sample-deployment + {:id "deployment/test-123" + :module "module/nginx-app" + :state "CREATED" + :parent "nuvlabox/edge-host-1" + :module/content {:name "NGINX Application"} + :module/author "test-provider"}) + + +(def sample-app-instance-info + {:appInstanceId "deployment/test-123" + :appDId "module/nginx-app" + :appName "NGINX Application" + :appProvider "test-provider" + :instantiationState "NOT_INSTANTIATED" + :mecHostInformation {:hostId "nuvlabox/edge-host-1" + :hostName "nuvlabox/edge-host-1"} + :_links {:self {:href "/app_lcm/v2/app_instances/deployment/test-123"} + :instantiate {:href "/app_lcm/v2/app_instances/deployment/test-123/instantiate"} + :terminate {:href "/app_lcm/v2/app_instances/deployment/test-123/terminate"} + :operate {:href "/app_lcm/v2/app_instances/deployment/test-123/operate"}}}) + + +(def sample-job + {:id "job/instantiate-123" + :state "RUNNING" + :operation-type "INSTANTIATE" + :target-resource "deployment/test-123" + :start-time "2025-10-21T10:00:00Z" + :state-entered-time "2025-10-21T10:00:05Z"}) + + +;; +;; Schema Validation Tests +;; + +(deftest test-app-instance-schema-validation + (testing "Valid AppInstanceInfo passes validation" + (is (= sample-app-instance-info + (app-instance/validate-app-instance-info sample-app-instance-info)))) + + (testing "AppInstanceInfo with missing appInstanceId fails" + (is (thrown? Exception + (app-instance/validate-app-instance-info + (dissoc sample-app-instance-info :appInstanceId))))) + + (testing "AppInstanceInfo with invalid state fails" + (is (thrown? Exception + (app-instance/validate-app-instance-info + (assoc sample-app-instance-info + :instantiationState "INVALID_STATE")))))) + + +(deftest test-app-lcm-op-occ-schema-validation + (testing "Valid AppLcmOpOcc passes validation" + (let [op-occ (app-lcm-op-occ/job->app-lcm-op-occ sample-job)] + (is (= op-occ (app-lcm-op-occ/validate-app-lcm-op-occ op-occ))))) + + (testing "AppLcmOpOcc with invalid operation type fails" + (is (thrown? Exception + (app-lcm-op-occ/validate-app-lcm-op-occ + {:lcmOpOccId "job/test" + :operationType "INVALID_OP" + :operationState "PROCESSING" + :stateEnteredTime "2025-10-21T10:00:00Z" + :startTime "2025-10-21T10:00:00Z" + :appInstanceId "deployment/test"}))))) + + +;; +;; State Mapping Tests +;; + +(deftest test-nuvla-to-mec-instantiation-state-mapping + (testing "CREATED maps to NOT_INSTANTIATED" + (is (= :NOT_INSTANTIATED + (get app-instance/nuvla-to-mec-instantiation-state :CREATED)))) + + (testing "STARTED maps to INSTANTIATED" + (is (= :INSTANTIATED + (get app-instance/nuvla-to-mec-instantiation-state :STARTED)))) + + (testing "STOPPED maps to INSTANTIATED" + (is (= :INSTANTIATED + (get app-instance/nuvla-to-mec-instantiation-state :STOPPED)))) + + (testing "ERROR maps to INSTANTIATED" + (is (= :INSTANTIATED + (get app-instance/nuvla-to-mec-instantiation-state :ERROR))))) + + +(deftest test-nuvla-to-mec-operational-state-mapping + (testing "STARTED maps to STARTED" + (is (= :STARTED + (get app-instance/nuvla-to-mec-operational-state :STARTED)))) + + (testing "STOPPED maps to STOPPED" + (is (= :STOPPED + (get app-instance/nuvla-to-mec-operational-state :STOPPED)))) + + (testing "STARTING has no operational state" + (is (nil? (get app-instance/nuvla-to-mec-operational-state :STARTING))))) + + +(deftest test-job-to-operation-state-mapping + (testing "RUNNING job maps to PROCESSING operation" + (is (= :PROCESSING + (get app-lcm-op-occ/nuvla-job-to-mec-operation-state :RUNNING)))) + + (testing "SUCCESS job maps to COMPLETED operation" + (is (= :COMPLETED + (get app-lcm-op-occ/nuvla-job-to-mec-operation-state :SUCCESS)))) + + (testing "FAILED job maps to FAILED operation" + (is (= :FAILED + (get app-lcm-op-occ/nuvla-job-to-mec-operation-state :FAILED))))) + + +;; +;; Translation Tests +;; + +(deftest test-deployment-to-app-instance-info-translation + (testing "Basic deployment translates correctly" + (let [result (app-instance/deployment->app-instance-info sample-deployment)] + (is (= "deployment/test-123" (:appInstanceId result))) + (is (= "module/nginx-app" (:appDId result))) + (is (= "NOT_INSTANTIATED" (:instantiationState result))) + (is (= "NGINX Application" (:appName result))) + (is (= "test-provider" (:appProvider result))) + (is (= "nuvlabox/edge-host-1" (get-in result [:mecHostInformation :hostId]))))) + + (testing "STARTED deployment has operational state" + (let [started-deployment (assoc sample-deployment :state "STARTED") + result (app-instance/deployment->app-instance-info started-deployment)] + (is (= "INSTANTIATED" (:instantiationState result))) + (is (= "STARTED" (:operationalState result))))) + + (testing "STOPPED deployment has operational state" + (let [stopped-deployment (assoc sample-deployment :state "STOPPED") + result (app-instance/deployment->app-instance-info stopped-deployment)] + (is (= "INSTANTIATED" (:instantiationState result))) + (is (= "STOPPED" (:operationalState result))))) + + (testing "HATEOAS links are generated" + (let [result (app-instance/deployment->app-instance-info sample-deployment)] + (is (contains? (:_links result) :self)) + (is (contains? (:_links result) :instantiate)) + (is (contains? (:_links result) :terminate)) + (is (contains? (:_links result) :operate))))) + + +(deftest test-app-instance-info-to-deployment-translation + (testing "AppInstanceInfo translates to deployment" + (let [result (app-instance/app-instance-info->deployment sample-app-instance-info)] + (is (= "deployment/test-123" (:id result))) + (is (= "module/nginx-app" (:module result))) + (is (= "CREATED" (:state result))) + (is (= "nuvlabox/edge-host-1" (:parent result)))))) + + +(deftest test-job-to-app-lcm-op-occ-translation + (testing "Job translates to AppLcmOpOcc" + (let [result (app-lcm-op-occ/job->app-lcm-op-occ sample-job)] + (is (= "job/instantiate-123" (:lcmOpOccId result))) + (is (= "INSTANTIATE" (:operationType result))) + (is (= "PROCESSING" (:operationState result))) + (is (= "deployment/test-123" (:appInstanceId result))) + (is (= "2025-10-21T10:00:00Z" (:startTime result))))) + + (testing "Failed job includes error information" + (let [failed-job (assoc sample-job + :state "FAILED" + :status-message "Deployment failed") + result (app-lcm-op-occ/job->app-lcm-op-occ failed-job)] + (is (= "FAILED" (:operationState result))) + (is (contains? result :error)) + (is (= "Deployment failed" (get-in result [:error :detail]))))) + + (testing "HATEOAS links are generated" + (let [result (app-lcm-op-occ/job->app-lcm-op-occ sample-job)] + (is (contains? (:_links result) :self)) + (is (contains? (:_links result) :appInstance))))) + + +;; +;; Error Handling Tests (RFC 7807) +;; + +(deftest test-problem-details-format + (testing "Not found error has correct structure" + (let [error (app-lcm-v2/not-found-error "deployment/missing")] + (is (= 404 (:status error))) + (is (= "Resource Not Found" (:title error))) + (is (contains? error :type)) + (is (contains? error :detail)) + (is (= "deployment/missing" (:instance error))))) + + (testing "Validation error has correct structure" + (let [error (app-lcm-v2/validation-error "Invalid input")] + (is (= 400 (:status error))) + (is (= "Validation Error" (:title error))) + (is (= "Invalid input" (:detail error))))) + + (testing "Conflict error has correct structure" + (let [error (app-lcm-v2/conflict-error "Resource already exists")] + (is (= 409 (:status error))) + (is (= "Resource Conflict" (:title error))) + (is (= "Resource already exists" (:detail error)))))) + + +;; +;; API Endpoint Tests +;; + +(deftest test-api-routes-defined + (testing "All required routes are defined" + (let [routes app-lcm-v2/routes + paths (map first routes)] + (is (some #(re-find #"/app_instances$" %) paths)) + (is (some #(re-find #"/app_instances/:id$" %) paths)) + (is (some #(re-find #"/instantiate$" %) paths)) + (is (some #(re-find #"/terminate$" %) paths)) + (is (some #(re-find #"/operate$" %) paths)) + (is (some #(re-find #"/app_lcm_op_occs$" %) paths)) + (is (some #(re-find #"/app_lcm_op_occs/:id$" %) paths))))) + + +(deftest test-base-uri + (testing "Base URI follows MEC 010-2 format" + (is (= "app_lcm/v2" app-lcm-v2/base-uri)))) + + +;; +;; Integration Tests Summary +;; + +(deftest test-phase-1-completion + (testing "Phase 1 Week 1 deliverables" + (is (= sample-app-instance-info + (app-instance/validate-app-instance-info sample-app-instance-info)) + "MEC 010-2 AppInstanceInfo schema defined and validation works") + + (let [op-occ (app-lcm-op-occ/job->app-lcm-op-occ sample-job)] + (is (= op-occ (app-lcm-op-occ/validate-app-lcm-op-occ op-occ)) + "MEC 010-2 AppLcmOpOcc schema defined and validation works")) + + (is (= :NOT_INSTANTIATED + (get app-instance/nuvla-to-mec-instantiation-state :CREATED)) + "State mapping functions work") + + (is (= "deployment/test-123" + (:appInstanceId (app-instance/deployment->app-instance-info sample-deployment))) + "Deployment to AppInstanceInfo translation works"))) + + +;; +;; Test Runner +;; + +(defn run-tests [] + (testing "MEC 010-2 Phase 1 Week 1 Tests" + (test-app-instance-schema-validation) + (test-app-lcm-op-occ-schema-validation) + (test-nuvla-to-mec-instantiation-state-mapping) + (test-nuvla-to-mec-operational-state-mapping) + (test-job-to-operation-state-mapping) + (test-deployment-to-app-instance-info-translation) + (test-app-instance-info-to-deployment-translation) + (test-job-to-app-lcm-op-occ-translation) + (test-problem-details-format) + (test-api-routes-defined) + (test-base-uri) + (test-phase-1-completion))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/app_package_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/app_package_lifecycle_test.clj new file mode 100644 index 000000000..8b92769c2 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/app_package_lifecycle_test.clj @@ -0,0 +1,219 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-package-lifecycle-test + "Integration tests for ETSI MEC 010-2 Mm1 Application Package Lifecycle" + (:require + [clojure.data.json :as json] + [clojure.test :refer [deftest is testing use-fixtures]] + [com.sixsq.nuvla.server.app.params :as p] + [com.sixsq.nuvla.server.middleware.authn-info :refer [authn-info-header]] + [com.sixsq.nuvla.server.resources.lifecycle-test-utils :as ltu] + [com.sixsq.nuvla.server.resources.module :as module] + [com.sixsq.nuvla.server.resources.module-application-mec :as mec] + [peridot.core :refer [content-type header request session]])) + + +(use-fixtures :each ltu/with-test-server-fixture) + + +(def base-uri (str p/service-context "/api/mec/app_lcm/v2")) + + +(deftest mm1-app-package-lifecycle + (let [session-admin (-> (ltu/ring-app) + session + (content-type "application/json") + (header authn-info-header "group/nuvla-admin group/nuvla-admin group/nuvla-anon")) + session-user (-> (ltu/ring-app) + session + (content-type "application/json") + (header authn-info-header "user/test-user user/test-user group/nuvla-anon"))] + + (testing "POST /app_packages - Create MEC application package" + (let [create-request {:appPkgName "test-mec-app" + :appPkgVersion "1.0.0" + :appProvider "Acme Corp" + :userDefinedData {:environment "testing" + :cost-center "R&D"}} + response (-> session-admin + (request (str base-uri "/app_packages") + :request-method :post + :body (json/write-str create-request)) + :response) + body (when (:body response) + (json/read-str (:body response) :key-fn keyword))] + + (is (= 201 (:status response))) + (is (string? (get-in response [:headers "Location"]))) + (is (= "test-mec-app" (:appName body))) + (is (= "Acme Corp" (:appProvider body))) + (is (= "1.0.0" (:appSoftVersion body))) + (is (= "CREATED" (:onboardingState body))) + (is (= "DISABLED" (:operationalState body))) + (is (= "NOT_IN_USE" (:usageState body))) + (is (map? (:_links body))) + + ;; Store package ID for subsequent tests + (def app-pkg-id (:appPkgId body)) + (def created-module-id (:id body)) + + (testing "GET /app_packages/{appPkgId} - Retrieve created package" + (let [get-response (-> session-admin + (request (str base-uri "/app_packages/" app-pkg-id) + :request-method :get) + :response) + get-body (json/read-str (:body get-response) :key-fn keyword)] + + (is (= 200 (:status get-response))) + (is (= app-pkg-id (:appPkgId get-body))) + (is (= "test-mec-app" (:appName get-body))) + (is (= "CREATED" (:onboardingState get-body))))) + + (testing "GET /app_packages - Query all packages" + (let [query-response (-> session-admin + (request (str base-uri "/app_packages") + :request-method :get) + :response) + query-body (json/read-str (:body query-response) :key-fn keyword) + packages (:AppPkgInfo query-body)] + + (is (= 200 (:status query-response))) + (is (vector? packages)) + (is (pos? (count packages))) + (is (some #(= app-pkg-id (:appPkgId %)) packages)))) + + (testing "GET /app_packages?appName=test-mec-app - Filter by name" + (let [query-response (-> session-admin + (request (str base-uri "/app_packages?appName=test-mec-app") + :request-method :get) + :response) + query-body (json/read-str (:body query-response) :key-fn keyword) + packages (:AppPkgInfo query-body)] + + (is (= 200 (:status query-response))) + (is (= 1 (count packages))) + (is (= "test-mec-app" (:appName (first packages)))))) + + (testing "GET /app_packages?appProvider=Acme Corp - Filter by provider" + (let [query-response (-> session-admin + (request (str base-uri "/app_packages?appProvider=Acme%20Corp") + :request-method :get) + :response) + query-body (json/read-str (:body query-response) :key-fn keyword) + packages (:AppPkgInfo query-body)] + + (is (= 200 (:status query-response))) + (is (some #(= "Acme Corp" (:appProvider %)) packages)))) + + (testing "DELETE /app_packages/{appPkgId} - Delete package" + (let [delete-response (-> session-admin + (request (str base-uri "/app_packages/" app-pkg-id) + :request-method :delete) + :response)] + + (is (= 204 (:status delete-response))))) + + (testing "GET /app_packages/{appPkgId} - Verify deletion (404)" + (let [get-response (-> session-admin + (request (str base-uri "/app_packages/" app-pkg-id) + :request-method :get) + :response)] + + (is (= 404 (:status get-response))))))))) + + +(deftest mm1-app-package-validation + (let [session-admin (-> (ltu/ring-app) + session + (content-type "application/json") + (header authn-info-header "group/nuvla-admin group/nuvla-admin group/nuvla-anon"))] + + (testing "POST /app_packages without appPkgName - Bad Request" + (let [invalid-request {:appPkgVersion "1.0.0"} + response (-> session-admin + (request (str base-uri "/app_packages") + :request-method :post + :body (json/write-str invalid-request)) + :response) + body (when (:body response) + (json/read-str (:body response) :key-fn keyword))] + + (is (= 400 (:status response))) + (is (= 400 (:status body))) + (is (string? (:detail body))) + (is (.contains (:detail body) "appPkgName")))) + + (testing "GET /app_packages/{nonexistent} - Not Found" + (let [response (-> session-admin + (request (str base-uri "/app_packages/module/nonexistent-123") + :request-method :get) + :response) + body (when (:body response) + (json/read-str (:body response) :key-fn keyword))] + + (is (= 404 (:status response))) + (is (= 404 (:status body))) + (is (= "Not Found" (:title body))))) + + (testing "DELETE /app_packages/{nonexistent} - Not Found" + (let [response (-> session-admin + (request (str base-uri "/app_packages/module/nonexistent-456") + :request-method :delete) + :response)] + + (is (= 404 (:status response))))))) + + +(deftest mm1-mec-module-integration + (let [session-admin (-> (ltu/ring-app) + session + (content-type "application/json") + (header authn-info-header "group/nuvla-admin group/nuvla-admin group/nuvla-anon"))] + + (testing "Create MEC module and verify it appears in Mm1 query" + ;; Create a module-application-mec directly + (let [mec-module {:name "Direct MEC Module" + :description "MEC module created directly" + :subtype "application_mec" + :path "test/mec-modules/direct" + :parent-path "test/mec-modules" + :published false + :content {:appName "Direct MEC App" + :appDId (str "appd-direct-" (random-uuid)) + :appProvider "Test Provider" + :appSoftVersion "2.0.0" + :appDVersion "3.2.1" + :mecVersion "2.2.1" + :virtualComputeDescriptor [{:virtualComputeDescId "compute-1" + :virtualCpu {:numVirtualCpu 2} + :virtualMemory {:virtualMemSize 2048}}] + :swImageDescriptor [] + :virtualStorageDescriptor [] + :appExtCpd [] + :appServiceRequired [] + :trafficRuleDescriptor [] + :dnsRuleDescriptor [] + :appFeatureRequired []}} + create-response (-> session-admin + (request (str p/service-context "/api/module") + :request-method :post + :body (json/write-str mec-module)) + :response) + module-id (get-in create-response [:body :resource-id])] + + (is (= 201 (:status create-response))) + (is (string? module-id)) + + ;; Query via Mm1 API + (let [query-response (-> session-admin + (request (str base-uri "/app_packages?appName=Direct%20MEC%20App") + :request-method :get) + :response) + query-body (json/read-str (:body query-response) :key-fn keyword) + packages (:AppPkgInfo query-body)] + + (is (= 200 (:status query-response))) + (is (some #(= "Direct MEC App" (:appName %)) packages))) + + ;; Cleanup + (-> session-admin + (request (str base-uri "/app_packages/" module-id) + :request-method :delete)))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/app_package_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/app_package_test.clj new file mode 100644 index 000000000..c0ac18b0d --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/app_package_test.clj @@ -0,0 +1,92 @@ +(ns com.sixsq.nuvla.server.resources.mec.app-package-test + "Tests for ETSI MEC 010-2 Mm1 Application Package Management API" + (:require + [clojure.test :refer [deftest is testing use-fixtures]] + [com.sixsq.nuvla.server.resources.mec.app-package :as t])) + + +(deftest test-module->app-pkg-info + (testing "Convert Nuvla module to MEC AppPkgInfo" + (let [module {:id "module/test-123" + :name "Test Application" + :subtype "application_mec" + :path "acme/apps/test" + :parent-path "acme/apps" + :published true + :versions [{:href "module/test-123/v1" :published true}] + :content {:appDId "module/test-123" + :appName "Test MEC App" + :appProvider "Acme Corp" + :appSoftVersion "1.2.3" + :appDVersion "1.0"} + :content-id "sha256:abc123" + :created "2024-01-15T10:00:00Z" + :updated "2024-01-15T10:00:00Z"} + app-pkg-info (t/module->app-pkg-info module)] + + (is (= "module/test-123" (:id app-pkg-info))) + (is (= "module/test-123" (:appPkgId app-pkg-info))) + (is (= "module/test-123" (:appDId app-pkg-info))) + (is (= "Test MEC App" (:appName app-pkg-info))) + (is (= "Acme Corp" (:appProvider app-pkg-info))) + (is (= "1.2.3" (:appSoftVersion app-pkg-info))) + (is (= "1.0" (:appDVersion app-pkg-info))) + (is (= "ENABLED" (:operationalState app-pkg-info))) + (is (= "IN_USE" (:usageState app-pkg-info))) + (is (= "ONBOARDED" (:onboardingState app-pkg-info))) + (is (= "acme/apps/test" (:appPkgPath app-pkg-info))) + (is (= "application_mec" (:moduletype app-pkg-info))))) + + (testing "Module without content uses fallback values" + (let [module {:id "module/simple-456" + :name "Simple App" + :subtype "application" + :path "apps/simple" + :parent-path "apps" + :published false + :versions [] + :created "2024-01-15T10:00:00Z" + :updated "2024-01-15T10:00:00Z"} + app-pkg-info (t/module->app-pkg-info module)] + + (is (= "module/simple-456" (:appDId app-pkg-info))) + (is (= "Simple App" (:appName app-pkg-info))) + (is (= "apps" (:appProvider app-pkg-info))) + (is (= "0" (:appSoftVersion app-pkg-info))) + (is (= "NOT_IN_USE" (:usageState app-pkg-info)))))) + + +(deftest test-app-pkg-filter + (testing "Base filter for MEC application modules" + (is (= "subtype='application_mec'" (t/app-pkg-filter nil)))) + + (testing "Filter with additional condition" + (is (= "subtype='application_mec' and published=true" + (t/app-pkg-filter "published=true"))))) + + +(deftest test-query-app-packages-request + (testing "Query request structure" + (let [request {:params {:appName "Test App" + :appProvider "Acme" + :usageState "IN_USE"}}] + ;; Test that parameters are extracted correctly + (is (= "Test App" (get-in request [:params :appName]))) + (is (= "Acme" (get-in request [:params :appProvider]))) + (is (= "IN_USE" (get-in request [:params :usageState])))))) + + +(deftest test-create-app-package-validation + (testing "CreateAppPkg requires appPkgName" + (let [body {:appPkgVersion "1.0.0"}] + (is (nil? (:appPkgName body))))) + + (testing "CreateAppPkg with all fields" + (let [body {:appPkgName "New App" + :appPkgVersion "2.0.0" + :appPkgPath "/custom/path" + :userDefinedData {:key1 "value1" :key2 "value2"}}] + (is (= "New App" (:appPkgName body))) + (is (= "2.0.0" (:appPkgVersion body))) + (is (= "/custom/path" (:appPkgPath body))) + (is (map? (:userDefinedData body)))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/error_handler_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/error_handler_test.clj new file mode 100644 index 000000000..33e4934c4 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/error_handler_test.clj @@ -0,0 +1,325 @@ +(ns com.sixsq.nuvla.server.resources.mec.error-handler-test + "Tests for RFC 7807 Error Handler" + (:require + [clojure.test :refer [deftest is testing]] + [com.sixsq.nuvla.server.resources.mec.error-handler :as eh])) + + +;; +;; Core ProblemDetails Tests +;; + +(deftest test-problem-details-minimal + (testing "Create minimal ProblemDetails with type, title, status" + (let [pd (eh/problem-details :not-found "Not Found" 404)] + (is (= "https://docs.nuvla.io/mec/errors/not-found" (:type pd))) + (is (= "Not Found" (:title pd))) + (is (= 404 (:status pd))) + (is (nil? (:detail pd))) + (is (nil? (:instance pd)))))) + + +(deftest test-problem-details-with-detail + (testing "Create ProblemDetails with detail" + (let [pd (eh/problem-details :validation-error "Validation Error" 400 "Missing required field")] + (is (= "https://docs.nuvla.io/mec/errors/validation" (:type pd))) + (is (= "Validation Error" (:title pd))) + (is (= 400 (:status pd))) + (is (= "Missing required field" (:detail pd)))))) + + +(deftest test-problem-details-with-instance + (testing "Create ProblemDetails with instance URI" + (let [pd (eh/problem-details :not-found "Not Found" 404 "Resource not found" "app-instance-123")] + (is (= "app-instance-123" (:instance pd)))))) + + +(deftest test-problem-details-with-extensions + (testing "Create ProblemDetails with extension fields" + (let [pd (eh/problem-details :conflict "Conflict" 409 "State conflict" "app-123" {:current-state "STARTED"})] + (is (= "STARTED" (:current-state pd)))))) + + +(deftest test-problem-details-custom-type-uri + (testing "Create ProblemDetails with custom type URI" + (let [pd (eh/problem-details "https://example.com/custom-error" "Custom" 418)] + (is (= "https://example.com/custom-error" (:type pd)))))) + + +;; +;; 4xx Client Error Tests +;; + +(deftest test-bad-request + (testing "Create 400 Bad Request error" + (let [pd (eh/bad-request "Invalid JSON")] + (is (= 400 (:status pd))) + (is (= "Bad Request" (:title pd))) + (is (= "Invalid JSON" (:detail pd)))))) + + +(deftest test-unauthorized + (testing "Create 401 Unauthorized error" + (let [pd (eh/unauthorized "Authentication required")] + (is (= 401 (:status pd))) + (is (= "Unauthorized" (:title pd)))))) + + +(deftest test-forbidden + (testing "Create 403 Forbidden error" + (let [pd (eh/forbidden "Insufficient permissions" "user-123")] + (is (= 403 (:status pd))) + (is (= "Forbidden" (:title pd))) + (is (= "user-123" (:instance pd)))))) + + +(deftest test-not-found + (testing "Create 404 Not Found error" + (let [pd (eh/not-found "AppInstance" "app-123")] + (is (= 404 (:status pd))) + (is (= "Resource Not Found" (:title pd))) + (is (= "AppInstance app-123 not found" (:detail pd))) + (is (= "app-123" (:instance pd)))))) + + +(deftest test-conflict + (testing "Create 409 Conflict error" + (let [pd (eh/conflict "Resource already exists" "app-123")] + (is (= 409 (:status pd))) + (is (= "Resource Conflict" (:title pd)))))) + + +(deftest test-invalid-state + (testing "Create 409 Invalid State error with state details" + (let [pd (eh/invalid-state "app-123" "STARTED" "STOPPED" "terminate")] + (is (= 409 (:status pd))) + (is (= "Invalid State for Operation" (:title pd))) + (is (= "app-123" (:instance pd))) + (is (= "STARTED" (:current-state pd))) + (is (= "STOPPED" (:expected-state pd))) + (is (= "terminate" (:operation pd)))))) + + +(deftest test-operation-not-allowed + (testing "Create 422 Operation Not Allowed error" + (let [pd (eh/operation-not-allowed "Cannot delete active instance")] + (is (= 422 (:status pd))) + (is (= "Operation Not Allowed" (:title pd)))))) + + +(deftest test-resource-exhausted + (testing "Create 507 Resource Exhausted error" + (let [pd (eh/resource-exhausted "CPU" "No available CPU resources")] + (is (= 507 (:status pd))) + (is (= "Resource Exhausted" (:title pd))) + (is (= "CPU" (:resource-type pd)))))) + + +;; +;; 5xx Server Error Tests +;; + +(deftest test-internal-error + (testing "Create 500 Internal Server Error" + (let [pd (eh/internal-error "Database connection failed")] + (is (= 500 (:status pd))) + (is (= "Internal Server Error" (:title pd))) + (is (= "Database connection failed" (:detail pd)))))) + + +(deftest test-mepm-error + (testing "Create 502 MEPM Error" + (let [pd (eh/mepm-error "http://mepm:8080" "MEPM returned invalid response")] + (is (= 502 (:status pd))) + (is (= "MEPM Communication Error" (:title pd))) + (is (= "http://mepm:8080" (:mepm-endpoint pd)))))) + + +(deftest test-service-unavailable + (testing "Create 503 Service Unavailable error" + (let [pd (eh/service-unavailable "Service temporarily down")] + (is (= 503 (:status pd))) + (is (= "Service Unavailable" (:title pd)))))) + + +(deftest test-gateway-timeout + (testing "Create 504 Gateway Timeout error" + (let [pd (eh/gateway-timeout "http://mepm:8080" "instantiate")] + (is (= 504 (:status pd))) + (is (= "Gateway Timeout" (:title pd))) + (is (= "http://mepm:8080" (:mepm-endpoint pd))) + (is (= "instantiate" (:operation pd)))))) + + +;; +;; Validation Error Helper Tests +;; + +(deftest test-validation-error + (testing "Create validation error for field" + (let [pd (eh/validation-error "appName" "Must not be empty")] + (is (= 400 (:status pd))) + (is (= "appName" (:field pd))) + (is (= "Must not be empty" (:reason pd)))))) + + +(deftest test-validation-error-with-value + (testing "Create validation error with invalid value" + (let [pd (eh/validation-error "cpu" "Must be positive" -1)] + (is (= "cpu" (:field pd))) + (is (= -1 (:value pd)))))) + + +(deftest test-missing-required-field + (testing "Create missing required field error" + (let [pd (eh/missing-required-field "appDId")] + (is (= 400 (:status pd))) + (is (= "appDId" (:field pd))) + (is (clojure.string/includes? (:detail pd) "Required field is missing"))))) + + +(deftest test-invalid-field-value + (testing "Create invalid field value error" + (let [pd (eh/invalid-field-value "operationalState" "UNKNOWN" "STARTED or STOPPED")] + (is (= "operationalState" (:field pd))) + (is (= "UNKNOWN" (:value pd))) + (is (clojure.string/includes? (:detail pd) "Expected: STARTED or STOPPED"))))) + + +(deftest test-invalid-enum-value + (testing "Create invalid enum value error" + (let [pd (eh/invalid-enum-value "operation" "DELETE" ["INSTANTIATE" "TERMINATE" "OPERATE"])] + (is (= "operation" (:field pd))) + (is (= "DELETE" (:value pd))) + (is (clojure.string/includes? (:detail pd) "INSTANTIATE"))))) + + +;; +;; Exception Handling Tests +;; + +(deftest test-exception-to-problem-details-with-status + (testing "Convert ExceptionInfo with :status to ProblemDetails" + (let [ex (ex-info "Resource not found" {:status 404 :resource-id "app-123"}) + pd (eh/exception->problem-details ex :instance "app-123")] + (is (= 404 (:status pd))) + (is (= "Resource Not Found" (:title pd))) + (is (= "Resource not found" (:detail pd))) + (is (= "app-123" (:instance pd))) + (is (= "app-123" (:resource-id pd)))))) + + +(deftest test-exception-to-problem-details-conflict + (testing "Convert 409 ExceptionInfo to ProblemDetails" + (let [ex (ex-info "State conflict" {:status 409 :current-state "STARTED"}) + pd (eh/exception->problem-details ex)] + (is (= 409 (:status pd))) + (is (= "Resource Conflict" (:title pd))) + (is (= "STARTED" (:current-state pd)))))) + + +(deftest test-exception-to-problem-details-generic + (testing "Convert generic exception to 500 ProblemDetails" + (let [ex (Exception. "Unexpected error") + pd (eh/exception->problem-details ex :operation "instantiate")] + (is (= 500 (:status pd))) + (is (= "Internal Server Error" (:title pd))) + (is (= "Unexpected error" (:detail pd))) + (is (= "instantiate" (:operation pd)))))) + + +(deftest test-exception-to-problem-details-with-operation + (testing "Convert exception with operation context" + (let [ex (ex-info "Operation failed" {:status 502}) + pd (eh/exception->problem-details ex :operation "terminate" :instance "app-123")] + (is (= 502 (:status pd))) + (is (= "app-123" (:instance pd))) + (is (= "terminate" (:operation pd)))))) + + +;; +;; Helper Function Tests +;; + +(deftest test-problem-details-predicate-valid + (testing "Recognize valid ProblemDetails map" + (let [pd (eh/not-found "AppInstance" "app-123")] + (is (eh/problem-details? pd))))) + + +(deftest test-problem-details-predicate-invalid-missing-fields + (testing "Reject map missing required fields" + (is (not (eh/problem-details? {:type "test" :title "Test"}))) + (is (not (eh/problem-details? {:status 404 :title "Test"}))))) + + +(deftest test-problem-details-predicate-invalid-status + (testing "Reject map with invalid status code" + (is (not (eh/problem-details? {:type "test" :title "Test" :status 200}))) ; 2xx not error + (is (not (eh/problem-details? {:type "test" :title "Test" :status 600}))))) ; > 599 + + +(deftest test-problem-details-predicate-not-map + (testing "Reject non-map values" + (is (not (eh/problem-details? "not a map"))) + (is (not (eh/problem-details? nil))) + (is (not (eh/problem-details? 404))))) + + +;; +;; Error Type URI Tests +;; + +(deftest test-error-type-uris-complete + (testing "Verify all error types have URIs" + (is (string? (eh/error-types :not-found))) + (is (string? (eh/error-types :validation-error))) + (is (string? (eh/error-types :conflict))) + (is (string? (eh/error-types :unauthorized))) + (is (string? (eh/error-types :forbidden))) + (is (string? (eh/error-types :operation-not-allowed))) + (is (string? (eh/error-types :invalid-state))) + (is (string? (eh/error-types :resource-exhausted))) + (is (string? (eh/error-types :mepm-error))) + (is (string? (eh/error-types :internal-error))) + (is (string? (eh/error-types :timeout))) + (is (string? (eh/error-types :bad-gateway))) + (is (string? (eh/error-types :service-unavailable))))) + + +(deftest test-error-type-uris-format + (testing "Verify error type URIs are well-formed" + (doseq [[_ uri] eh/error-types] + (is (clojure.string/starts-with? uri "https://")) + (is (clojure.string/includes? uri "nuvla.io/mec/errors/"))))) + + +;; +;; Module Completeness Test +;; + +(deftest test-error-handler-complete + (testing "Verify all expected functions are available" + (let [expected-fns ['problem-details + 'bad-request + 'unauthorized + 'forbidden + 'not-found + 'conflict + 'invalid-state + 'operation-not-allowed + 'resource-exhausted + 'internal-error + 'mepm-error + 'service-unavailable + 'gateway-timeout + 'validation-error + 'missing-required-field + 'invalid-field-value + 'invalid-enum-value + 'exception->problem-details + 'log-and-return-error + 'problem-details?]] + (doseq [fn-name expected-fns] + (is (some? (ns-resolve 'com.sixsq.nuvla.server.resources.mec.error-handler fn-name)) + (str "Function " fn-name " should be defined")))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/integration_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/integration_test.clj new file mode 100644 index 000000000..545e2f489 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/integration_test.clj @@ -0,0 +1,164 @@ +(ns com.sixsq.nuvla.server.resources.mec.integration-test + "Integration tests for MEC 010-2 Application Lifecycle Management API + + Tests validate end-to-end workflows across multiple MEC modules to ensure + all components work together correctly for standards-compliant MEC orchestration. + + Standard: ETSI GS MEC 010-2 v2.2.1" + (:require + [clojure.test :refer [deftest is testing]] + [com.sixsq.nuvla.server.resources.mec.lifecycle-handler :as lifecycle] + [com.sixsq.nuvla.server.resources.mec.app-lcm-subscription :as subscription] + [com.sixsq.nuvla.server.resources.mec.error-handler :as error])) + + +;; +;; Test Fixtures +;; + +(def test-app-instance-id "deployment/test-integration-app") +(def test-mepm-endpoint "http://localhost:8080/mepm") + + +;; +;; Integration Test 1: Basic Lifecycle Flow +;; + +(deftest test-basic-lifecycle-flow + (testing "Complete lifecycle: instantiate → operate → terminate" + (let [instantiate-op (lifecycle/instantiate test-app-instance-id {})] + (is (= "INSTANTIATE" (:operationType instantiate-op))) + (is (contains? instantiate-op :lcmOpOccId)) + + (let [operate-op (lifecycle/operate test-app-instance-id {:changeStateTo "STOPPED"})] + (is (= "OPERATE" (:operationType operate-op))) + (is (= test-app-instance-id (:appInstanceId operate-op))) + + (let [terminate-op (lifecycle/terminate test-app-instance-id {:terminationType "GRACEFUL"})] + (is (= "TERMINATE" (:operationType terminate-op))) + (is (contains? terminate-op :lcmOpOccId))))))) + + +;; +;; Integration Test 2: Multiple Operations Tracking +;; + +(deftest test-multiple-operations-tracking + (testing "Track multiple operations for same app instance" + (let [ops [(lifecycle/instantiate test-app-instance-id {}) + (lifecycle/operate test-app-instance-id {:changeStateTo "STOPPED"}) + (lifecycle/operate test-app-instance-id {:changeStateTo "STARTED"}) + (lifecycle/terminate test-app-instance-id {:terminationType "GRACEFUL"})]] + (is (= 4 (count ops))) + (is (every? #(contains? % :lcmOpOccId) ops)) + (is (every? #(= test-app-instance-id (:appInstanceId %)) ops)) + (is (= ["INSTANTIATE" "OPERATE" "OPERATE" "TERMINATE"] + (map :operationType ops)))))) + + +;; +;; Integration Test 3: Subscription Creation +;; + +(deftest test-subscription-creation + (testing "Create and validate subscription" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "https://webhook.example.com/notifications" + {:operationalState "STARTED"} + "user/test-user")] + (is (contains? sub :id)) + (is (= "AppInstanceStateChangeNotification" (:subscription-type sub))) + (is (= "https://webhook.example.com/notifications" (:callback-uri sub)))))) + + +;; +;; Integration Test 4: Error Handling +;; + +(deftest test-error-handling-integration + (testing "Error handling across modules" + (let [error-response (error/not-found "AppInstance not-found-123" "not-found-123")] + (is (error/problem-details? error-response)) + (is (= 404 (:status error-response))) + (is (= "not-found-123" (:instance error-response)))) + + (let [validation-error (error/validation-error "Invalid state transition" + {:current "NOT_INSTANTIATED" + :attempted "TERMINATE"})] + (is (error/problem-details? validation-error)) + (is (= 400 (:status validation-error)))))) + + +;; +;; Integration Test 5: Cross-Module Data Flow +;; + +(deftest test-cross-module-data-flow + (testing "Data flows correctly across lifecycle, subscription, and error modules" + ;; Lifecycle creates operation + (let [op-occ (lifecycle/instantiate test-app-instance-id {})] + (is (contains? op-occ :lcmOpOccId)) + (is (contains? op-occ :_links)) + + ;; Subscription can reference operation + (let [sub (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "https://example.com/webhook" + {:operationType "INSTANTIATE"} + "user/test-user")] + (is (contains? sub :id)) + (is (= "INSTANTIATE" (get-in sub [:app-lcm-op-occ-filter :operationType]))) + + ;; Error handler can report issues + (let [error (error/internal-error "Operation failed" + nil + {:lcmOpOccId (:lcmOpOccId op-occ)})] + (is (error/problem-details? error)) + (is (= 500 (:status error))) + ;; Extensions are merged directly into the error map + (is (contains? error :lcmOpOccId))))))) + + +;; +;; Integration Test 6: HATEOAS Links Consistency +;; + +(deftest test-hateoas-links-consistency + (testing "HATEOAS links are consistent across operations" + (let [op-occ (lifecycle/instantiate test-app-instance-id {})] + (is (contains? (:_links op-occ) :self)) + (is (contains? (:_links op-occ) :appInstance)) + (is (re-find #"/app_lcm/v2/app_lcm_op_occs/" (get-in op-occ [:_links :self :href]))) + (is (re-find #"/app_lcm/v2/app_instances/" (get-in op-occ [:_links :appInstance :href])))))) + + +;; +;; Integration Test 7: Operation State Consistency +;; + +(deftest test-operation-state-consistency + (testing "Operation states are consistent across lifecycle" + (let [ops [(lifecycle/instantiate test-app-instance-id {}) + (lifecycle/terminate test-app-instance-id {:terminationType "GRACEFUL"})]] + (doseq [op ops] + (is (contains? op :operationState)) + (is (contains? #{"PROCESSING" "STARTING" "FAILED"} (:operationState op))) + (is (contains? op :startTime)) + (is (contains? op :stateEnteredTime)))))) + + +;; +;; Integration Test 8: Module Completeness +;; + +(deftest test-integration-module-completeness + (testing "Integration test module has all required test categories" + (let [test-ns (find-ns 'com.sixsq.nuvla.server.resources.mec.integration-test) + tests (filter #(clojure.string/starts-with? (str %) "test-") + (keys (ns-publics test-ns)))] + (is (>= (count tests) 8) "Should have at least 8 integration test scenarios") + (is (some #(clojure.string/includes? (str %) "lifecycle") tests)) + (is (some #(clojure.string/includes? (str %) "subscription") tests)) + (is (some #(clojure.string/includes? (str %) "error") tests)) + (is (some #(clojure.string/includes? (str %) "cross-module") tests))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/lifecycle_handler_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/lifecycle_handler_test.clj new file mode 100644 index 000000000..e229a7984 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/lifecycle_handler_test.clj @@ -0,0 +1,234 @@ +(ns com.sixsq.nuvla.server.resources.mec.lifecycle-handler-test + "Tests for MEC 010-2 Lifecycle Operation Handler + + Tests cover: + - Instantiate operation flow + - Terminate operation flow + - Operate operation flow (start/stop) + - MEPM endpoint resolution + - Error handling and validation + + Standard: ETSI GS MEC 010-2 v2.2.1" + (:require + [clojure.test :refer [deftest is testing]] + [com.sixsq.nuvla.server.resources.mec.lifecycle-handler :as lifecycle])) + + +;; +;; Test Fixtures +;; + +(def test-app-instance-id "deployment/test-app-123") +(def test-mepm-endpoint "http://localhost:8080/mepm") + + +;; +;; MEPM Endpoint Resolution Tests +;; + +(deftest test-resolve-mepm-endpoint + (testing "MEPM endpoint resolution returns valid URL" + (let [endpoint (lifecycle/resolve-mepm-endpoint test-app-instance-id)] + (is (string? endpoint)) + (is (re-find #"^http" endpoint)) + (is (= test-mepm-endpoint endpoint))))) + + +;; +;; Operation Context Tests +;; + +(deftest test-create-operation-context + (testing "Operation context creation" + (let [context (lifecycle/create-operation-context + :INSTANTIATE + test-app-instance-id + {:grantId "grant-123"})] + (is (= "INSTANTIATE" (:operation-type context))) + (is (= test-app-instance-id (:app-instance-id context))) + (is (= {:grantId "grant-123"} (:request-params context))) + (is (= :STARTING (:status context))) + (is (string? (:start-time context)))))) + + +;; +;; Instantiate Operation Tests +;; + +(deftest test-instantiate-operation-structure + (testing "Instantiate creates proper operation occurrence structure" + (let [request-params {:grantId "grant-123"} + op-occ (lifecycle/instantiate test-app-instance-id request-params)] + (is (contains? op-occ :lcmOpOccId)) + (is (= "INSTANTIATE" (:operationType op-occ))) + (is (contains? #{"PROCESSING" "FAILED"} (:operationState op-occ))) + (is (= test-app-instance-id (:appInstanceId op-occ))) + (is (contains? op-occ :stateEnteredTime)) + (is (contains? op-occ :startTime)) + (is (contains? op-occ :_links))))) + + +(deftest test-instantiate-operation-links + (testing "Instantiate operation has proper HATEOAS links" + (let [op-occ (lifecycle/instantiate test-app-instance-id {})] + (is (contains? (:_links op-occ) :self)) + (is (contains? (:_links op-occ) :appInstance)) + (is (re-find #"/app_lcm/v2/app_lcm_op_occs/" (get-in op-occ [:_links :self :href]))) + (is (re-find #"/app_lcm/v2/app_instances/" (get-in op-occ [:_links :appInstance :href])))))) + + +;; +;; Terminate Operation Tests +;; + +(deftest test-terminate-operation-structure + (testing "Terminate creates proper operation occurrence structure" + (let [request-params {:terminationType "FORCEFUL"} + op-occ (lifecycle/terminate test-app-instance-id request-params)] + (is (contains? op-occ :lcmOpOccId)) + (is (= "TERMINATE" (:operationType op-occ))) + (is (contains? #{"PROCESSING" "FAILED"} (:operationState op-occ))) + (is (= test-app-instance-id (:appInstanceId op-occ))) + (is (contains? op-occ :stateEnteredTime)) + (is (contains? op-occ :startTime)) + (is (contains? op-occ :_links))))) + + +(deftest test-terminate-operation-links + (testing "Terminate operation has proper HATEOAS links" + (let [op-occ (lifecycle/terminate test-app-instance-id {})] + (is (contains? (:_links op-occ) :self)) + (is (contains? (:_links op-occ) :appInstance))))) + + +;; +;; Operate Operation Tests +;; + +(deftest test-operate-operation-structure + (testing "Operate creates proper operation occurrence structure" + (let [request-params {:changeStateTo "STARTED"} + op-occ (lifecycle/operate test-app-instance-id request-params)] + (is (contains? op-occ :lcmOpOccId)) + (is (= "OPERATE" (:operationType op-occ))) + (is (contains? #{"PROCESSING" "FAILED"} (:operationState op-occ))) + (is (= test-app-instance-id (:appInstanceId op-occ))) + (is (contains? op-occ :stateEnteredTime)) + (is (contains? op-occ :startTime)) + (is (contains? op-occ :_links))))) + + +(deftest test-operate-valid-states + (testing "Operate accepts STARTED state" + (let [op-occ (lifecycle/operate test-app-instance-id {:changeStateTo "STARTED"})] + (is (contains? op-occ :lcmOpOccId)))) + + (testing "Operate accepts STOPPED state" + (let [op-occ (lifecycle/operate test-app-instance-id {:changeStateTo "STOPPED"})] + (is (contains? op-occ :lcmOpOccId)))) + + (testing "Operate accepts keyword STARTED" + (let [op-occ (lifecycle/operate test-app-instance-id {:changeStateTo :STARTED})] + (is (contains? op-occ :lcmOpOccId))))) + + +;; +;; Error Handling Tests +;; + +(deftest test-operation-error-handling + (testing "Failed operations include error information" + ;; Note: This test assumes MEPM is unavailable, causing failure + (let [op-occ (lifecycle/instantiate test-app-instance-id {})] + (when (= "FAILED" (:operationState op-occ)) + (is (contains? op-occ :error)) + (is (contains? (:error op-occ) :type)) + (is (contains? (:error op-occ) :title)) + (is (contains? (:error op-occ) :status)) + (is (contains? (:error op-occ) :detail)) + (is (= (:lcmOpOccId op-occ) (get-in op-occ [:error :instance]))))))) + + +;; +;; Operation ID Generation Tests +;; + +(deftest test-operation-id-uniqueness + (testing "Each operation gets a unique ID" + (let [op1 (lifecycle/instantiate test-app-instance-id {}) + op2 (lifecycle/instantiate test-app-instance-id {}) + op3 (lifecycle/terminate test-app-instance-id {})] + (is (not= (:lcmOpOccId op1) (:lcmOpOccId op2))) + (is (not= (:lcmOpOccId op1) (:lcmOpOccId op3))) + (is (not= (:lcmOpOccId op2) (:lcmOpOccId op3)))))) + + +(deftest test-operation-id-format + (testing "Operation IDs follow job/* format" + (let [op-occ (lifecycle/instantiate test-app-instance-id {})] + (is (re-find #"^job/" (:lcmOpOccId op-occ)))))) + + +;; +;; Timestamp Tests +;; + +(deftest test-operation-timestamps + (testing "Operations have valid timestamps" + (let [op-occ (lifecycle/instantiate test-app-instance-id {})] + (is (string? (:startTime op-occ))) + (is (string? (:stateEnteredTime op-occ))) + ;; Basic ISO8601 format check + (is (re-find #"\d{4}-\d{2}-\d{2}T" (:startTime op-occ))) + (is (re-find #"\d{4}-\d{2}-\d{2}T" (:stateEnteredTime op-occ)))))) + + +;; +;; Integration Test Summary +;; + +(deftest test-phase-2-week-4-completion + (testing "Phase 2 Week 4 deliverables" + (testing "Instantiate endpoint operational" + (let [op-occ (lifecycle/instantiate test-app-instance-id {:grantId "test"})] + (is (= "INSTANTIATE" (:operationType op-occ))))) + + (testing "Terminate endpoint operational" + (let [op-occ (lifecycle/terminate test-app-instance-id {})] + (is (= "TERMINATE" (:operationType op-occ))))) + + (testing "Operate endpoint operational" + (let [op-occ (lifecycle/operate test-app-instance-id {:changeStateTo "STARTED"})] + (is (= "OPERATE" (:operationType op-occ))))) + + (testing "Operation occurrence tracking works" + (let [op-occ (lifecycle/instantiate test-app-instance-id {})] + (is (contains? op-occ :lcmOpOccId)) + (is (contains? op-occ :operationState)) + (is (contains? op-occ :appInstanceId)))) + + (testing "MEPM endpoint resolution works" + (let [endpoint (lifecycle/resolve-mepm-endpoint test-app-instance-id)] + (is (string? endpoint)) + (is (re-find #"^http" endpoint)))))) + + +;; +;; Test Runner +;; + +(defn run-tests [] + (testing "MEC 010-2 Phase 2 Week 4 Tests" + (test-resolve-mepm-endpoint) + (test-create-operation-context) + (test-instantiate-operation-structure) + (test-instantiate-operation-links) + (test-terminate-operation-structure) + (test-terminate-operation-links) + (test-operate-operation-structure) + (test-operate-valid-states) + (test-operation-error-handling) + (test-operation-id-uniqueness) + (test-operation-id-format) + (test-operation-timestamps) + (test-phase-2-week-4-completion))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/mm3_client_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/mm3_client_test.clj new file mode 100644 index 000000000..bbce76cb1 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/mm3_client_test.clj @@ -0,0 +1,192 @@ +(ns com.sixsq.nuvla.server.resources.mec.mm3-client-test + (:require + [clojure.test :refer [deftest is testing use-fixtures]] + [com.sixsq.nuvla.server.resources.mec.mm3-client :as mm3] + [clj-http.client :as http])) + + +;; Mock HTTP responses +(def mock-health-success + {:status 200 + :body {:status "healthy" + :timestamp "2025-10-21T14:00:00Z" + :uptime-seconds 86400}}) + +(def mock-health-failure + {:status 503 + :body {:status "unhealthy" + :message "Service unavailable"}}) + +(def mock-capabilities-success + {:status 200 + :body {:platforms ["x86_64" "arm64"] + :services ["rnis" "location" "wai"] + :api-version "3.1.1"}}) + +(def mock-resources-success + {:status 200 + :body {:cpu-cores 64 + :memory-gb 256 + :storage-gb 2000 + :gpu-count 4}}) + + +(deftest test-check-health-success + (testing "Successful health check" + (with-redefs [http/get (fn [_url _opts] mock-health-success)] + (let [result (mm3/check-health "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (:success? result)) + (is (= 200 (:status result))) + (is (= "healthy" (get-in result [:data :status]))))))) + + +(deftest test-check-health-failure + (testing "Failed health check returns proper error" + (with-redefs [http/get (fn [_url _opts] mock-health-failure)] + (let [result (mm3/check-health "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (not (:success? result))) + (is (= 503 (:status result))) + (is (= :server-error (:error result))))))) + + +(deftest test-check-health-connection-error + (testing "Connection error during health check" + (with-redefs [http/get (fn [_url _opts] + (throw (Exception. "Connection refused")))] + (let [result (mm3/check-health "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (not (:success? result))) + (is (= :connection-error (:error result))) + (is (some? (:exception result))))))) + + +(deftest test-query-capabilities-success + (testing "Successful capabilities query" + (with-redefs [http/get (fn [_url _opts] mock-capabilities-success)] + (let [result (mm3/query-capabilities "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (:success? result)) + (is (= 200 (:status result))) + (is (= ["x86_64" "arm64"] (get-in result [:data :platforms]))) + (is (= ["rnis" "location" "wai"] (get-in result [:data :services]))))))) + + +(deftest test-query-capabilities-failure + (testing "Failed capabilities query" + (with-redefs [http/get (fn [_url _opts] {:status 404 :body {:message "Not found"}})] + (let [result (mm3/query-capabilities "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (not (:success? result))) + (is (= 404 (:status result))) + (is (= :client-error (:error result))))))) + + +(deftest test-query-resources-success + (testing "Successful resources query" + (with-redefs [http/get (fn [_url _opts] mock-resources-success)] + (let [result (mm3/query-resources "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (:success? result)) + (is (= 200 (:status result))) + (is (= 64 (get-in result [:data :cpu-cores]))) + (is (= 256 (get-in result [:data :memory-gb]))) + (is (= 4 (get-in result [:data :gpu-count]))))))) + + +(deftest test-configure-platform-success + (testing "Successful platform configuration" + (with-redefs [http/post (fn [_url _opts] {:status 200 :body {:status "configured"}})] + (let [config {:service-registry true :traffic-rules false} + result (mm3/configure-platform "https://mepm.example.com:8443" config {:retry-attempts 1})] + (is (:success? result)) + (is (= 200 (:status result))))))) + + +(deftest test-get-platform-info-success + (testing "Successful platform info retrieval" + (with-redefs [http/get (fn [_url _opts] + {:status 200 + :body {:name "MEPM-001" + :version "1.0.0" + :location "edge-site-1" + :status "ONLINE"}})] + (let [result (mm3/get-platform-info "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (:success? result)) + (is (= "MEPM-001" (get-in result [:data :name]))) + (is (= "ONLINE" (get-in result [:data :status]))))))) + + +(deftest test-retry-mechanism + (testing "Retry mechanism on transient failures" + (let [call-count (atom 0)] + (with-redefs [http/get (fn [_url _opts] + (swap! call-count inc) + (if (< @call-count 3) + (throw (Exception. "Transient error")) + mock-health-success))] + (let [result (mm3/check-health "https://mepm.example.com:8443" {:retry-attempts 3})] + (is (:success? result)) + (is (= 3 @call-count) "Should retry exactly 3 times")))))) + + +(deftest test-healthy-predicate + (testing "healthy? convenience function" + (with-redefs [http/get (fn [_url _opts] mock-health-success)] + (is (true? (mm3/healthy? "https://mepm.example.com:8443" {:retry-attempts 1})))) + + (with-redefs [http/get (fn [_url _opts] mock-health-failure)] + (is (false? (mm3/healthy? "https://mepm.example.com:8443" {:retry-attempts 1})))))) + + +(deftest test-get-capabilities-convenience + (testing "get-capabilities convenience function returns data or nil" + (with-redefs [http/get (fn [_url _opts] mock-capabilities-success)] + (let [caps (mm3/get-capabilities "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (some? caps)) + (is (= ["x86_64" "arm64"] (:platforms caps))))) + + (with-redefs [http/get (fn [_url _opts] {:status 500 :body {}})] + (is (nil? (mm3/get-capabilities "https://mepm.example.com:8443" {:retry-attempts 1})))))) + + +(deftest test-get-resources-convenience + (testing "get-resources convenience function returns data or nil" + (with-redefs [http/get (fn [_url _opts] mock-resources-success)] + (let [resources (mm3/get-resources "https://mepm.example.com:8443" {:retry-attempts 1})] + (is (some? resources)) + (is (= 64 (:cpu-cores resources))))) + + (with-redefs [http/get (fn [_url _opts] {:status 500 :body {}})] + (is (nil? (mm3/get-resources "https://mepm.example.com:8443" {:retry-attempts 1})))))) + + +(deftest test-url-construction + (testing "URLs are correctly constructed" + (let [captured-urls (atom [])] + (with-redefs [http/get (fn [url _opts] + (swap! captured-urls conj url) + mock-health-success)] + (mm3/check-health "https://mepm.example.com:8443" {:retry-attempts 1}) + (mm3/query-capabilities "https://mepm.example.com:8443" {:retry-attempts 1}) + (mm3/query-resources "https://mepm.example.com:8443" {:retry-attempts 1}) + (mm3/get-platform-info "https://mepm.example.com:8443" {:retry-attempts 1}) + + (is (= "https://mepm.example.com:8443/mm3/health" (first @captured-urls))) + (is (= "https://mepm.example.com:8443/mm3/capabilities" (second @captured-urls))) + (is (= "https://mepm.example.com:8443/mm3/resources" (nth @captured-urls 2))) + (is (= "https://mepm.example.com:8443/mm3/platform-info" (nth @captured-urls 3))))))) + + +(deftest test-http-options + (testing "HTTP options are properly configured" + (let [captured-opts (atom nil)] + (with-redefs [http/get (fn [_url opts] + (reset! captured-opts opts) + mock-health-success)] + (mm3/check-health "https://mepm.example.com:8443" + {:timeout 60000 + :connect-timeout 20000 + :insecure? true + :retry-attempts 1}) + + (is (= 60000 (:socket-timeout @captured-opts))) + (is (= 20000 (:connection-timeout @captured-opts))) + (is (true? (:insecure? @captured-opts))) + (is (= :json (:as @captured-opts))) + (is (= :json (:content-type @captured-opts))))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/mm3_integration_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/mm3_integration_test.clj new file mode 100644 index 000000000..2eec2e478 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/mm3_integration_test.clj @@ -0,0 +1,289 @@ +(ns com.sixsq.nuvla.server.resources.mec.mm3-integration-test + "Integration tests for Mm3 interface using mock MEPM server." + (:require + [clojure.test :refer [deftest is testing use-fixtures]] + [com.sixsq.nuvla.server.resources.mec.mm3-client :as mm3] + [com.sixsq.nuvla.server.resources.mec.mock-mepm-server :as mock-mepm])) + +(def test-port 18080) +(def test-endpoint (str "http://localhost:" test-port)) + +;; +;; Test Fixtures +;; + +(defn start-mock-mepm-fixture [f] + (mock-mepm/start-server! test-port) + (try + (f) + (finally + (mock-mepm/stop-server!)))) + +(use-fixtures :once start-mock-mepm-fixture) + +(defn reset-mepm-state-fixture [f] + (mock-mepm/reset-state!) + (f)) + +(use-fixtures :each reset-mepm-state-fixture) + +;; +;; Mm3 Protocol Validation Tests +;; + +(deftest test-mm5-health-check-protocol + (testing "Health check follows ETSI MEC 003 protocol" + (let [response (mm3/check-health test-endpoint)] + (is (:success? response)) + (is (= 200 (:status response))) + (is (contains? (:data response) :status)) + (is (contains? (:data response) :timestamp)) + (is (contains? (:data response) :version)) + (is (contains? (:data response) :checks))))) + +(deftest test-mm5-capabilities-protocol + (testing "Capabilities query follows ETSI MEC 003 protocol" + (let [response (mm3/query-capabilities test-endpoint)] + (is (:success? response)) + (is (= 200 (:status response))) + (is (contains? (:data response) :platforms)) + (is (contains? (:data response) :services)) + (is (contains? (:data response) :api-version)) + (is (vector? (:platforms (:data response)))) + (is (vector? (:services (:data response))))))) + +(deftest test-mm5-resources-protocol + (testing "Resources query follows ETSI MEC 003 protocol" + (let [response (mm3/query-resources test-endpoint)] + (is (:success? response)) + (is (= 200 (:status response))) + (is (contains? (:data response) :cpu-cores)) + (is (contains? (:data response) :memory-gb)) + (is (contains? (:data response) :storage-gb)) + (is (pos? (:cpu-cores (:data response)))) + (is (pos? (:memory-gb (:data response))))))) + +(deftest test-mm5-platform-info-protocol + (testing "Platform info follows ETSI MEC 003 protocol" + (let [response (mm3/get-platform-info test-endpoint)] + (is (:success? response)) + (is (= 200 (:status response))) + (is (contains? (:data response) :platform-id)) + (is (contains? (:data response) :platform-name)) + (is (contains? (:data response) :location))))) + +(deftest test-mm5-configure-platform-protocol + (testing "Platform configuration follows ETSI MEC 003 protocol" + (let [config {:dns-rules [{:domain "example.com" :ip "10.0.0.1"}] + :traffic-rules [{:priority 1 :action "allow"}]} + response (mm3/configure-platform test-endpoint config)] + (is (:success? response)) + (is (= 200 (:status response))) + (is (contains? (:data response) :success)) + (is (:success (:data response)))))) + +;; +;; Error Handling Tests +;; + +(deftest test-mm5-timeout-handling + (testing "Graceful handling of MEPM timeout" + (mock-mepm/set-error-mode! :timeout) + (let [response (mm3/check-health test-endpoint {:retry-attempts 1})] + (is (not (:success? response))) + (is (= 504 (:status response)))))) + +(deftest test-mm5-server-error-handling + (testing "Graceful handling of MEPM server error" + (mock-mepm/set-error-mode! :server-error) + (let [response (mm3/query-capabilities test-endpoint {:retry-attempts 1})] + (is (not (:success? response))) + (is (= 500 (:status response)))))) + +(deftest test-mm5-not-found-handling + (testing "Graceful handling of MEPM not found" + (mock-mepm/set-error-mode! :not-found) + (let [response (mm3/query-resources test-endpoint {:retry-attempts 1})] + (is (not (:success? response))) + (is (= 404 (:status response)))))) + +(deftest test-mm5-degraded-service + (testing "Graceful handling of degraded MEPM" + (mock-mepm/set-error-mode! :degraded) + (let [response (mm3/check-health test-endpoint {:retry-attempts 1})] + (is (not (:success? response))) + (is (= 503 (:status response)))))) + +;; +;; Retry Mechanism Tests +;; + +(deftest test-mm5-retry-mechanism-simulation + (testing "Mock server can simulate transient failures" + ;; This test verifies the mock server error mode works correctly + ;; Actual retry testing is done implicitly in other tests + (mock-mepm/set-error-mode! :server-error) + (let [response1 (mm3/check-health test-endpoint {:retry-attempts 1})] + (is (not (:success? response1)))) + + ;; Reset error mode and verify recovery + (mock-mepm/set-error-mode! nil) + (let [response2 (mm3/check-health test-endpoint)] + (is (:success? response2))))) + +(deftest test-mm5-connection-refused + (testing "Graceful handling of connection refused" + (let [bad-endpoint "http://localhost:9999"] + (let [response (mm3/check-health bad-endpoint {:retry-attempts 1 + :connect-timeout 1000})] + (is (not (:success? response))) + (is (= :connection-error (:error response))))))) + +;; +;; End-to-End Flow Tests +;; + +(deftest test-mm5-full-health-check-flow + (testing "Complete health check flow" + ;; Check health + (let [health-response (mm3/check-health test-endpoint)] + (is (:success? health-response)) + (is (= "online" (:status (:data health-response))))) + + ;; Use convenience function + (is (mm3/healthy? test-endpoint)))) + +(deftest test-mm5-full-capability-query-flow + (testing "Complete capability query flow" + ;; Query capabilities + (let [cap-response (mm3/query-capabilities test-endpoint)] + (is (:success? cap-response)) + (is (seq (:platforms (:data cap-response)))) + (is (seq (:services (:data cap-response))))) + + ;; Use convenience function + (let [capabilities (mm3/get-capabilities test-endpoint)] + (is (some? capabilities)) + (is (contains? capabilities :platforms)) + (is (contains? capabilities :services))))) + +(deftest test-mm5-full-resource-query-flow + (testing "Complete resource query flow" + ;; Query resources + (let [res-response (mm3/query-resources test-endpoint)] + (is (:success? res-response)) + (is (pos? (:cpu-cores (:data res-response)))) + (is (pos? (:memory-gb (:data res-response))))) + + ;; Use convenience function + (let [resources (mm3/get-resources test-endpoint)] + (is (some? resources)) + (is (contains? resources :cpu-cores)) + (is (contains? resources :memory-gb))))) + +;; +;; Application Lifecycle Tests +;; + +(deftest test-mm5-app-instance-creation + (testing "Create application instance via Mm3" + (let [app-desc {:name "test-app" + :image "nginx:latest" + :resources {:cpu 2 :memory 4}} + create-response (mm3/create-app-instance test-endpoint app-desc)] + (is (:success? create-response)) + (is (= 201 (:status create-response))) + (let [app-id (:id (:data create-response))] + (is (some? app-id)) + + ;; Query app instance + (let [get-response (mm3/get-app-instance test-endpoint app-id)] + (is (:success? get-response)) + (is (= "test-app" (:name (:data get-response)))) + (is (= "INSTANTIATED" (:status (:data get-response))))) + + ;; Delete app instance + (let [delete-response (mm3/delete-app-instance test-endpoint app-id)] + (is (:success? delete-response)) + (is (= 204 (:status delete-response)))) + + ;; Verify deletion + (let [get-after-delete (mm3/get-app-instance test-endpoint app-id)] + (is (not (:success? get-after-delete))) + (is (= 404 (:status get-after-delete)))))))) + +(deftest test-mm5-list-app-instances + (testing "List application instances via Mm3" + ;; Initially empty + (let [list-response (mm3/list-app-instances test-endpoint)] + (is (:success? list-response)) + (is (empty? (:instances (:data list-response))))) + + ;; Create two instances + (mm3/create-app-instance test-endpoint {:name "app-1"}) + (mm3/create-app-instance test-endpoint {:name "app-2"}) + + ;; List should show both + (let [list-response (mm3/list-app-instances test-endpoint)] + (is (:success? list-response)) + (is (= 2 (count (:instances (:data list-response)))))))) + +;; +;; HTTP Options Tests +;; + +(deftest test-mm5-custom-timeouts + (testing "Custom timeout options work" + (let [response (mm3/check-health test-endpoint + {:timeout 5000 + :connect-timeout 2000})] + (is (:success? response))))) + +(deftest test-mm5-insecure-option + (testing "Insecure SSL option works" + (let [response (mm3/check-health test-endpoint + {:insecure? true})] + (is (:success? response))))) + +;; +;; Performance Tests +;; + +(deftest test-mm5-concurrent-requests + (testing "Handle concurrent requests correctly" + (let [futures (doall + (for [i (range 10)] + (future + (mm3/check-health test-endpoint))))] + (let [results (map deref futures)] + (is (every? :success? results)) + (is (= 10 (count results))))))) + +(deftest test-mm5-request-counting + (testing "Mock server counts requests correctly" + (mock-mepm/reset-state!) + (mm3/check-health test-endpoint) + (mm3/query-capabilities test-endpoint) + (mm3/query-resources test-endpoint) + (let [state (mock-mepm/get-state)] + (is (= 3 (:request-count state)))))) + +;; +;; Integration with MEPM State +;; + +(deftest test-mm5-reflects-mepm-state-changes + (testing "Mm3 client reflects MEPM state changes" + ;; Initial state + (let [initial-response (mm3/query-capabilities test-endpoint)] + (is (some? (:platforms (:data initial-response))))) + + ;; Modify MEPM state + (swap! @#'mock-mepm/mepm-state + assoc :capabilities {:platforms ["new-platform"] + :services ["new-service"]}) + + ;; Query should reflect changes + (let [updated-response (mm3/query-capabilities test-endpoint)] + (is (= ["new-platform"] (:platforms (:data updated-response)))) + (is (= ["new-service"] (:services (:data updated-response))))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/mock_mepm_server.clj b/code/test/com/sixsq/nuvla/server/resources/mec/mock_mepm_server.clj new file mode 100644 index 000000000..972c8f54c --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/mock_mepm_server.clj @@ -0,0 +1,341 @@ +(ns com.sixsq.nuvla.server.resources.mec.mock-mepm-server + "Mock MEPM server for testing Mm5 interface. + Implements ETSI MEC 003 Mm5 reference point for integration testing." + (:require + [clojure.tools.logging :as log] + [ring.adapter.jetty :as jetty] + [ring.middleware.json :refer [wrap-json-body wrap-json-response]] + [ring.middleware.params :refer [wrap-params]] + [ring.util.response :as response])) + +;; +;; Mock MEPM State +;; + +(defonce ^:private mepm-state + (atom {:status :online + :capabilities {:platforms ["kubernetes" "docker"] + :services ["app-lifecycle" "traffic-rules"] + :api-version "3.1.1" + :mec-version "3.1.1" + :supported-vnfds ["container" "vm"]} + :resources {:cpu-cores 64 + :memory-gb 256 + :storage-gb 2000 + :gpu-count 4 + :available {:cpu-cores 32 + :memory-gb 128 + :storage-gb 1000 + :gpu-count 2}} + :app-instances {} + :request-count 0 + :error-mode nil})) + +(defn reset-state! + "Reset MEPM state to defaults." + [] + (reset! mepm-state {:status :online + :capabilities {:platforms ["kubernetes" "docker"] + :services ["app-lifecycle" "traffic-rules"] + :api-version "3.1.1" + :mec-version "3.1.1" + :supported-vnfds ["container" "vm"]} + :resources {:cpu-cores 64 + :memory-gb 256 + :storage-gb 2000 + :gpu-count 4 + :available {:cpu-cores 32 + :memory-gb 128 + :storage-gb 1000 + :gpu-count 2}} + :app-instances {} + :request-count 0 + :error-mode nil})) + +(defn set-error-mode! + "Set error mode for testing error handling. + Modes: :timeout, :server-error, :not-found, nil (normal)" + [mode] + (swap! mepm-state assoc :error-mode mode)) + +(defn get-state + "Get current MEPM state." + [] + @mepm-state) + +;; +;; Mm5 Endpoint Handlers +;; + +(defn- increment-request-count! [] + (swap! mepm-state update :request-count inc)) + +(defn- check-error-mode + "Check if we should simulate an error based on error-mode." + [] + (when-let [mode (:error-mode @mepm-state)] + (case mode + :timeout {:status 504 :body {:error "Gateway Timeout" :message "MEPM not responding"}} + :server-error {:status 500 :body {:error "Internal Server Error" :message "MEPM internal error"}} + :not-found {:status 404 :body {:error "Not Found" :message "MEPM not found"}} + :degraded {:status 503 :body {:error "Service Unavailable" :message "MEPM degraded"}} + nil))) + +(defn handle-health-check + "Handle GET /mm5/health - Check MEPM health status." + [_request] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + (let [state @mepm-state] + {:status 200 + :body {:status (name (:status state)) + :timestamp (str (java.time.Instant/now)) + :version "1.0.0" + :uptime-seconds 3600 + :checks {:database "healthy" + :mep "healthy" + :vim "healthy"}}}))) + +(defn handle-capabilities-query + "Handle GET /mm5/capabilities - Query MEPM capabilities." + [_request] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + {:status 200 + :body (:capabilities @mepm-state)})) + +(defn handle-resources-query + "Handle GET /mm5/resources - Query available resources." + [_request] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + {:status 200 + :body (:resources @mepm-state)})) + +(defn handle-platform-info + "Handle GET /mm5/platform-info - Get platform metadata." + [_request] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + {:status 200 + :body {:platform-id "mock-mepm-1" + :platform-name "Mock MEPM Server" + :location {:latitude 48.8566 + :longitude 2.3522 + :city "Paris" + :country "France"} + :operator "Mock Operator" + :created "2025-01-01T00:00:00Z"}})) + +(defn handle-configure-platform + "Handle POST /mm5/configure - Configure platform settings." + [request] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + (let [config (:body request)] + (log/info "Configuring platform with:" config) + {:status 200 + :body {:success true + :message "Platform configured successfully" + :config config}}))) + +(defn handle-create-app-instance + "Handle POST /mm5/app-instances - Create application instance." + [request] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + (let [app-desc (:body request) + app-id (str "app-" (java.util.UUID/randomUUID)) + instance {:id app-id + :name (:name app-desc) + :status "INSTANTIATED" + :created (str (java.time.Instant/now)) + :descriptor app-desc}] + (swap! mepm-state assoc-in [:app-instances app-id] instance) + {:status 201 + :body instance}))) + +(defn handle-get-app-instance + "Handle GET /mm5/app-instances/:id - Get application instance status." + [request app-id] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + (let [instance (get-in @mepm-state [:app-instances app-id])] + (if instance + {:status 200 + :body instance} + {:status 404 + :body {:error "Not Found" :message (str "Application instance " app-id " not found")}})))) + +(defn handle-list-app-instances + "Handle GET /mm5/app-instances - List all application instances." + [_request] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + {:status 200 + :body {:instances (vals (:app-instances @mepm-state))}})) + +(defn handle-delete-app-instance + "Handle DELETE /mm5/app-instances/:id - Terminate application instance." + [request app-id] + (increment-request-count!) + (if-let [error-response (check-error-mode)] + error-response + (if (get-in @mepm-state [:app-instances app-id]) + (do + (swap! mepm-state update :app-instances dissoc app-id) + {:status 204}) + {:status 404 + :body {:error "Not Found" :message (str "Application instance " app-id " not found")}}))) + +;; +;; Router +;; + +(defn- route-request + "Route incoming requests to appropriate handlers." + [request] + (let [method (:request-method request) + path (:uri request)] + (log/debug "Mock MEPM received:" method path) + (try + (cond + ;; Health check + (and (= method :get) (= path "/mm5/health")) + (handle-health-check request) + + ;; Capabilities + (and (= method :get) (= path "/mm5/capabilities")) + (handle-capabilities-query request) + + ;; Resources + (and (= method :get) (= path "/mm5/resources")) + (handle-resources-query request) + + ;; Platform info + (and (= method :get) (= path "/mm5/platform-info")) + (handle-platform-info request) + + ;; Configure platform + (and (= method :post) (= path "/mm5/configure")) + (handle-configure-platform request) + + ;; App instances - list (must come before single instance match) + (and (= method :get) (= path "/mm5/app-instances")) + (handle-list-app-instances request) + + ;; App instances - create + (and (= method :post) (= path "/mm5/app-instances")) + (handle-create-app-instance request) + + ;; App instances - get single + (and (= method :get) (re-matches #"/mm5/app-instances/(.+)" path)) + (let [app-id (second (re-matches #"/mm5/app-instances/(.+)" path))] + (handle-get-app-instance request app-id)) + + ;; App instances - delete + (and (= method :delete) (re-matches #"/mm5/app-instances/(.+)" path)) + (let [app-id (second (re-matches #"/mm5/app-instances/(.+)" path))] + (handle-delete-app-instance request app-id)) + + ;; Not found + :else + {:status 404 + :body {:error "Not Found" :message (str "Unknown endpoint: " method " " path)}}) + (catch Exception e + (log/error e "Error handling request") + {:status 500 + :body {:error "Internal Server Error" :message (.getMessage e)}})))) + +(defn- wrap-logging [handler] + (fn [request] + (log/debug "Mock MEPM Request:" (:request-method request) (:uri request)) + (let [response (handler request)] + (log/debug "Mock MEPM Response:" (:status response)) + response))) + +(defn create-handler + "Create Ring handler for mock MEPM server." + [] + (-> route-request + (wrap-json-body {:keywords? true}) + wrap-json-response + wrap-params + wrap-logging)) + +;; +;; Server Lifecycle +;; + +(defonce ^:private server (atom nil)) + +(declare stop-server!) + +(defn start-server! + "Start mock MEPM server on specified port." + ([port] + (start-server! port {})) + ([port options] + (when @server + (stop-server!)) + (reset-state!) + (log/info "Starting mock MEPM server on port" port) + (let [server-instance (jetty/run-jetty + (create-handler) + (merge {:port port + :join? false} + options))] + (reset! server server-instance) + (log/info "Mock MEPM server started on port" port) + server-instance))) + +(defn stop-server! + "Stop mock MEPM server." + [] + (when-let [s @server] + (log/info "Stopping mock MEPM server") + (.stop s) + (reset! server nil) + (log/info "Mock MEPM server stopped"))) + +(defn restart-server! + "Restart mock MEPM server on specified port." + ([port] + (restart-server! port {})) + ([port options] + (stop-server!) + (Thread/sleep 100) ; Brief pause to ensure port is released + (start-server! port options))) + +(defn server-running? + "Check if mock MEPM server is running." + [] + (some? @server)) + +;; +;; Test Utilities +;; + +(defn with-mock-mepm + "Test fixture to run tests with mock MEPM server. + Usage: (with-mock-mepm 8080 (fn [] (run-tests)))" + [port test-fn] + (try + (start-server! port) + (test-fn) + (finally + (stop-server!)))) + +(defmacro with-mock-mepm-server + "Macro to run tests with mock MEPM server. + Usage: (with-mock-mepm-server 8080 (test-something))" + [port & body] + `(with-mock-mepm ~port (fn [] ~@body))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/notification_dispatcher_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/notification_dispatcher_test.clj new file mode 100644 index 000000000..66063943e --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/notification_dispatcher_test.clj @@ -0,0 +1,414 @@ +(ns com.sixsq.nuvla.server.resources.mec.notification-dispatcher-test + "Tests for MEC 010-2 Notification Dispatcher" + (:require + [clojure.test :refer [deftest is testing use-fixtures]] + [com.sixsq.nuvla.server.resources.mec.app-lcm-subscription :as subscription] + [com.sixsq.nuvla.server.resources.mec.notification-dispatcher :as dispatcher])) + + +;; +;; Test Helpers +;; + +(def delivered-notifications (atom [])) + + +(defn reset-test-state! + "Reset test state" + [] + (reset! delivered-notifications []) + (dispatcher/reset-delivery-stats!)) + + +(use-fixtures :each + (fn [f] + (reset-test-state!) + (f))) + + +;; +;; Test Data +;; + +(def test-user-id "user/test-user") + +(def test-app-instance + {:id "deployment/abc-123" + :app-name "test-app" + :app-d-id "appd/test-1" + :instantiation-state "INSTANTIATED" + :operational-state "STARTED"}) + +(def test-app-lcm-op-occ + {:id "job/op-123" + :app-instance-id "deployment/abc-123" + :operation-type "INSTANTIATE" + :operation-state "COMPLETED" + :start-time (java.time.Instant/parse "2025-01-01T10:00:00Z") + :state-entered-time (java.time.Instant/parse "2025-01-01T10:05:00Z")}) + + +;; +;; Webhook Delivery Tests (Using invalid endpoints to test error handling) +;; + +(deftest test-dispatch-notification-failure + (testing "Dispatch notification to invalid endpoint fails gracefully" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/invalid" ; Invalid port + {} + test-user-id) + notification (subscription/build-app-instance-notification + sub + test-app-instance + "OPERATIONAL_STATE" + "STOPPED") + result (dispatcher/dispatch-notification sub notification)] + + ;; Should fail but not throw + (is (false? (:success? result))) + (is (contains? #{:connection-error :unexpected-error} (:error result))) + + ;; Check stats + (let [stats (dispatcher/get-delivery-stats)] + (is (= 1 (:total-sent stats))) + (is (= 0 (:successful stats))) + (is (= 1 (:failed stats))) + (is (>= (:retries stats) 2)))))) ; At least 2 retries + + +(deftest test-dispatch-notification-async-returns-future + (testing "Dispatch notification asynchronously returns future" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/invalid" + {} + test-user-id) + notification (subscription/build-app-instance-notification + sub + test-app-instance + "OPERATIONAL_STATE" + "STOPPED") + future-result (dispatcher/dispatch-notification-async sub notification)] + + ;; Future should be created immediately + (is (future? future-result)) + + ;; Wait for async delivery (will fail but shouldn't throw) + (let [result @future-result] + (is (false? (:success? result))))))) + + +;; +;; App Instance State Change Tests +;; + +(deftest test-handle-app-instance-state-change-no-subscriptions + (testing "Handle app instance state change with no subscriptions" + (let [subscriptions [] + futures (dispatcher/handle-app-instance-state-change + subscriptions + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + ;; No subscriptions = no notifications + (is (empty? futures))))) + + +(deftest test-handle-app-instance-state-change-matching-subscription + (testing "Handle app instance state change with matching subscription" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/webhook" ; Will fail but that's OK for test + {:app-name "test-app"} + test-user-id) + subscriptions [sub] + futures (dispatcher/handle-app-instance-state-change + subscriptions + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + ;; One matching subscription + (is (= 1 (count futures))) + (is (every? future? futures)) + + ;; Wait for delivery + (doseq [f futures] @f) + + ;; Check stats - notification was attempted + (let [stats (dispatcher/get-delivery-stats)] + (is (= 1 (:total-sent stats))))))) + + +(deftest test-handle-app-instance-state-change-non-matching-subscription + (testing "Handle app instance state change with non-matching subscription" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/webhook" + {:app-name "different-app"} ; Does not match + test-user-id) + subscriptions [sub] + futures (dispatcher/handle-app-instance-state-change + subscriptions + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + ;; No matching subscriptions + (is (empty? futures))))) + + +(deftest test-handle-app-instance-state-change-inactive-subscription + (testing "Handle app instance state change with inactive subscription" + (let [sub (subscription/deactivate-subscription + (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/webhook" + {} + test-user-id)) + subscriptions [sub] + futures (dispatcher/handle-app-instance-state-change + subscriptions + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + ;; Inactive subscription should not match + (is (empty? futures))))) + + +(deftest test-handle-app-instance-state-change-multiple-subscriptions + (testing "Handle app instance state change with multiple subscriptions" + (let [sub1 (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/webhook1" + {:operational-state "STARTED"} + test-user-id) + sub2 (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/webhook2" + {:app-name "test-app"} + test-user-id) + sub3 (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" ; Wrong type + "http://localhost:99999/webhook3" + {} + test-user-id) + subscriptions [sub1 sub2 sub3] + futures (dispatcher/handle-app-instance-state-change + subscriptions + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + ;; Two matching subscriptions (sub1 and sub2, not sub3) + (is (= 2 (count futures))) + + ;; Wait for delivery + (doseq [f futures] @f) + + ;; Check stats - 2 notifications attempted + (let [stats (dispatcher/get-delivery-stats)] + (is (= 2 (:total-sent stats))))))) + + +;; +;; App LCM Op Occ State Change Tests +;; + +(deftest test-handle-app-lcm-op-occ-state-change-matching-subscription + (testing "Handle operation state change with matching subscription" + (let [sub (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "http://localhost:99999/webhook" + {:operation-type "INSTANTIATE"} + test-user-id) + subscriptions [sub] + futures (dispatcher/handle-app-lcm-op-occ-state-change + subscriptions + test-app-lcm-op-occ + "OPERATION_STATE" + "PROCESSING")] + + ;; One matching subscription + (is (= 1 (count futures))) + + ;; Wait for delivery + (doseq [f futures] @f) + + ;; Check stats - notification attempted + (let [stats (dispatcher/get-delivery-stats)] + (is (= 1 (:total-sent stats))))))) + + +(deftest test-handle-app-lcm-op-occ-state-change-filter-by-state + (testing "Handle operation state change filtered by operation state" + (let [sub (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "http://localhost:99999/webhook" + {:operation-state "FAILED"} ; Does not match COMPLETED + test-user-id) + subscriptions [sub] + futures (dispatcher/handle-app-lcm-op-occ-state-change + subscriptions + test-app-lcm-op-occ + "OPERATION_STATE" + "PROCESSING")] + + ;; No matching subscriptions + (is (empty? futures))))) + + +;; +;; Manual Triggering Tests +;; + +(deftest test-trigger-app-instance-notification + (testing "Manually trigger app instance notification" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/webhook" + {} + test-user-id) + subscriptions [sub] + futures (dispatcher/trigger-app-instance-notification + subscriptions + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + (is (= 1 (count futures))) + + ;; Wait for delivery + (doseq [f futures] @f) + + ;; Check stats + (let [stats (dispatcher/get-delivery-stats)] + (is (= 1 (:total-sent stats))))))) + + +(deftest test-trigger-app-lcm-op-occ-notification + (testing "Manually trigger operation occurrence notification" + (let [sub (subscription/create-subscription + "AppLcmOpOccStateChangeNotification" + "http://localhost:99999/webhook" + {} + test-user-id) + subscriptions [sub] + futures (dispatcher/trigger-app-lcm-op-occ-notification + subscriptions + test-app-lcm-op-occ + "OPERATION_STATE" + "PROCESSING")] + + (is (= 1 (count futures))) + + ;; Wait for delivery + (doseq [f futures] @f) + + ;; Check stats + (let [stats (dispatcher/get-delivery-stats)] + (is (= 1 (:total-sent stats))))))) + + +;; +;; Delivery Stats Tests +;; + +(deftest test-delivery-stats + (testing "Track delivery statistics" + (let [sub-fail (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/invalid" + {} + test-user-id) + notification (subscription/build-app-instance-notification + sub-fail + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + ;; Reset stats + (dispatcher/reset-delivery-stats!) + (is (= 0 (:total-sent (dispatcher/get-delivery-stats)))) + + ;; Failed delivery + (dispatcher/dispatch-notification sub-fail notification) + + ;; Check stats + (let [stats (dispatcher/get-delivery-stats)] + (is (= 1 (:total-sent stats))) + (is (= 0 (:successful stats))) + (is (= 1 (:failed stats))) + (is (>= (:retries stats) 2)))))) + + +(deftest test-reset-delivery-stats + (testing "Reset delivery statistics" + (let [sub (subscription/create-subscription + "AppInstanceStateChangeNotification" + "http://localhost:99999/webhook" + {} + test-user-id) + notification (subscription/build-app-instance-notification + sub + test-app-instance + "OPERATIONAL_STATE" + "STOPPED")] + + ;; Send notification + (dispatcher/dispatch-notification sub notification) + (is (= 1 (:total-sent (dispatcher/get-delivery-stats)))) + + ;; Reset + (dispatcher/reset-delivery-stats!) + (is (= 0 (:total-sent (dispatcher/get-delivery-stats)))) + (is (= 0 (:successful (dispatcher/get-delivery-stats)))) + (is (= 0 (:failed (dispatcher/get-delivery-stats)))) + (is (= 0 (:retries (dispatcher/get-delivery-stats))))))) + + +;; +;; Event Listener Tests +;; + +(deftest test-start-stop-event-listener + (testing "Start and stop event listener (stub)" + (let [subscription-store (atom []) + listener (dispatcher/start-event-listener + subscription-store + {:kafka-brokers ["localhost:9092"] + :topics ["deployment-events" "job-events"] + :group-id "mec-notifications"})] + + ;; Should return listener handle + (is (some? listener)) + (is (= :stub (:type listener))) + (is (some? (:started-at listener))) + + ;; Stop listener + (is (nil? (dispatcher/stop-event-listener listener)))))) + + +;; +;; Module Completeness Test +;; + +(deftest test-notification-dispatcher-complete + (testing "Verify all expected functions are available" + (let [expected-fns ['dispatch-notification + 'dispatch-notification-async + 'handle-app-instance-state-change + 'handle-app-lcm-op-occ-state-change + 'start-event-listener + 'stop-event-listener + 'trigger-app-instance-notification + 'trigger-app-lcm-op-occ-notification + 'get-delivery-stats + 'reset-delivery-stats!]] + (doseq [fn-name expected-fns] + (is (some? (ns-resolve 'com.sixsq.nuvla.server.resources.mec.notification-dispatcher fn-name)) + (str "Function " fn-name " should be defined")))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/orchestration_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/orchestration_test.clj new file mode 100644 index 000000000..f96396df3 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/orchestration_test.clj @@ -0,0 +1,220 @@ +(ns com.sixsq.nuvla.server.resources.mec.orchestration-test + "End-to-end orchestration tests for MEC MEO functionality." + (:require + [clojure.test :refer [deftest is testing use-fixtures]] + [jsonista.core :as json] + [com.sixsq.nuvla.server.app.params :as p] + [com.sixsq.nuvla.server.middleware.authn-info :refer [authn-info-header]] + [com.sixsq.nuvla.server.resources.mec.mm5-client :as mm5] + [com.sixsq.nuvla.server.resources.mec.mock-mepm-server :as mock-mepm] + [com.sixsq.nuvla.server.resources.mepm :as mepm] + [com.sixsq.nuvla.server.resources.lifecycle-test-utils :as ltu] + [peridot.core :refer [content-type header request session]])) + +(def base-uri (str p/service-context mepm/resource-type)) +(def test-port 18081) +(def test-endpoint (str "http://localhost:" test-port)) + +(defn start-mock-mepm-fixture [f] + (mock-mepm/start-server! test-port) + (try + (f) + (finally + (mock-mepm/stop-server!)))) + +(use-fixtures :once ltu/with-test-server-fixture start-mock-mepm-fixture) + +(defn reset-mepm-state-fixture [f] + (mock-mepm/reset-state!) + (f)) + +(use-fixtures :each reset-mepm-state-fixture) + +(deftest test-complete-mepm-orchestration-flow + (testing "Complete MEPM lifecycle" + (let [session-anon (-> (ltu/ring-app) + session + (content-type "application/json")) + session-admin (header session-anon authn-info-header + "group/nuvla-admin group/nuvla-admin group/nuvla-user group/nuvla-anon")] + + (let [mepm-data {:name "Orchestration Test MEPM" + :description "End-to-end test MEPM" + :endpoint test-endpoint + :capabilities {:platforms ["kubernetes"] + :services ["mec-service-1"]}} + resp-create (-> session-admin + (request base-uri + :request-method :post + :body (json/write-value-as-string mepm-data)) + (ltu/body->edn) + (ltu/is-status 201)) + mepm-id (ltu/body-resource-id resp-create) + mepm-url (str p/service-context mepm-id)] + + (let [mepm (-> session-admin + (request mepm-url) + (ltu/body->edn) + (ltu/is-status 200) + :response + :body)] + (is (= "Orchestration Test MEPM" (:name mepm))) + (is (= "ONLINE" (:status mepm)))) + + ;; Perform health check + (-> session-admin + (request (str mepm-url "/check-health") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200)) + + (let [mepm-after-health (-> session-admin + (request mepm-url) + (ltu/body->edn) + :response + :body)] + (is (= "ONLINE" (:status mepm-after-health)))) + + ;; Query capabilities + (-> session-admin + (request (str mepm-url "/query-capabilities") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200)) + + (let [mepm-with-caps (-> session-admin + (request mepm-url) + (ltu/body->edn) + :response + :body)] + (is (some? (:capabilities mepm-with-caps))) + (is (vector? (get-in mepm-with-caps [:capabilities :platforms])))) + + ;; Query resources + (-> session-admin + (request (str mepm-url "/query-resources") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200)) + + (let [mepm-with-res (-> session-admin + (request mepm-url) + (ltu/body->edn) + :response + :body)] + (is (some? (:resources mepm-with-res))) + (is (pos? (get-in mepm-with-res [:resources :cpu-cores])))) + + (-> session-admin + (request mepm-url :request-method :delete) + (ltu/body->edn) + (ltu/is-status 200)) + + (-> session-admin + (request mepm-url) + (ltu/body->edn) + (ltu/is-status 404)))))) + +(deftest test-multiple-mepm-management + (testing "Register and list multiple MEPMs" + (let [session-anon (-> (ltu/ring-app) + session + (content-type "application/json")) + session-admin (header session-anon authn-info-header + "group/nuvla-admin group/nuvla-admin group/nuvla-user group/nuvla-anon")] + + (let [mepm1-data {:name "MEPM-1" :endpoint test-endpoint + :capabilities {:platforms ["kubernetes"]}} + resp1 (-> session-admin + (request base-uri + :request-method :post + :body (json/write-value-as-string mepm1-data)) + (ltu/body->edn) + (ltu/is-status 201)) + mepm1-id (ltu/body-resource-id resp1)] + + (let [mepm2-data {:name "MEPM-2" :endpoint test-endpoint + :capabilities {:platforms ["kubernetes"]}} + resp2 (-> session-admin + (request base-uri + :request-method :post + :body (json/write-value-as-string mepm2-data)) + (ltu/body->edn) + (ltu/is-status 201)) + mepm2-id (ltu/body-resource-id resp2)] + + (let [search-resp (-> session-admin + (request base-uri) + (ltu/body->edn) + (ltu/is-status 200)) + mepms (get-in search-resp [:response :body :resources])] + (is (>= (count mepms) 2))) + + (-> session-admin + (request (str p/service-context mepm1-id) :request-method :delete) + (ltu/body->edn)) + (-> session-admin + (request (str p/service-context mepm2-id) :request-method :delete) + (ltu/body->edn))))))) + +(deftest test-mepm-health-degradation-recovery + (testing "MEPM health status transitions" + (let [session-anon (-> (ltu/ring-app) + session + (content-type "application/json")) + session-admin (header session-anon authn-info-header + "group/nuvla-admin group/nuvla-admin group/nuvla-user group/nuvla-anon")] + + (let [mepm-data {:name "Health Test MEPM" :endpoint test-endpoint + :capabilities {:platforms ["kubernetes"]}} + resp (-> session-admin + (request base-uri + :request-method :post + :body (json/write-value-as-string mepm-data)) + (ltu/body->edn) + (ltu/is-status 201)) + mepm-id (ltu/body-resource-id resp) + mepm-url (str p/service-context mepm-id)] + + ;; Health check should succeed + (-> session-admin + (request (str mepm-url "/check-health") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200)) + + (is (= "ONLINE" (:status (-> session-admin (request mepm-url) (ltu/body->edn) :response :body)))) + + ;; Simulate degradation + (mock-mepm/set-error-mode! :degraded) + + ;; Health check should now fail with 503 + (-> session-admin + (request (str mepm-url "/check-health") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 503)) + + (is (= "DEGRADED" (:status (-> session-admin (request mepm-url) (ltu/body->edn) :response :body)))) + + ;; Recover from degradation + (mock-mepm/set-error-mode! nil) + + ;; Health check should succeed again + (-> session-admin + (request (str mepm-url "/check-health") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200)) + + (is (= "ONLINE" (:status (-> session-admin (request mepm-url) (ltu/body->edn) :response :body)))) + + (-> session-admin + (request mepm-url :request-method :delete) + (ltu/body->edn)))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mec/query_filter_test.clj b/code/test/com/sixsq/nuvla/server/resources/mec/query_filter_test.clj new file mode 100644 index 000000000..dbf687c2c --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mec/query_filter_test.clj @@ -0,0 +1,315 @@ +(ns com.sixsq.nuvla.server.resources.mec.query-filter-test + "Tests for MEC 010-2 Query Filter Parser" + (:require + [clojure.test :refer [deftest is testing]] + [com.sixsq.nuvla.server.resources.mec.query-filter :as qf])) + + +;; +;; Test Data +;; + +(def test-resources + [{:id "app-1" :appName "web-app" :operationalState "STARTED" :cpu 2 :memory 4096} + {:id "app-2" :appName "api-service" :operationalState "STOPPED" :cpu 4 :memory 8192} + {:id "app-3" :appName "web-app" :operationalState "STARTED" :cpu 1 :memory 2048} + {:id "app-4" :appName "database" :operationalState "STARTED" :cpu 8 :memory 16384} + {:id "app-5" :appName "cache" :operationalState "STOPPED" :cpu 2 :memory 4096}]) + + +;; +;; Filter Parsing Tests +;; + +(deftest test-parse-filter-eq + (testing "Parse equality filter" + (let [filter-expr (qf/parse-filter "(eq,appName,web-app)")] + (is (= :eq (:op filter-expr))) + (is (= :appName (:field filter-expr))) + (is (= "web-app" (:value filter-expr)))))) + + +(deftest test-parse-filter-neq + (testing "Parse not-equal filter" + (let [filter-expr (qf/parse-filter "(neq,operationalState,STOPPED)")] + (is (= :neq (:op filter-expr))) + (is (= :operationalState (:field filter-expr))) + (is (= "STOPPED" (:value filter-expr)))))) + + +(deftest test-parse-filter-gt + (testing "Parse greater-than filter" + (let [filter-expr (qf/parse-filter "(gt,cpu,2)")] + (is (= :gt (:op filter-expr))) + (is (= :cpu (:field filter-expr))) + (is (= "2" (:value filter-expr)))))) + + +(deftest test-parse-filter-lt + (testing "Parse less-than filter" + (let [filter-expr (qf/parse-filter "(lt,memory,8192)")] + (is (= :lt (:op filter-expr))) + (is (= :memory (:field filter-expr))) + (is (= "8192" (:value filter-expr)))))) + + +(deftest test-parse-filter-in + (testing "Parse in-set filter" + (let [filter-expr (qf/parse-filter "(in,appName,web-app,api-service,database)")] + (is (= :in (:op filter-expr))) + (is (= :appName (:field filter-expr))) + (is (= ["web-app" "api-service" "database"] (:values filter-expr)))))) + + +(deftest test-parse-filter-and + (testing "Parse AND filter" + (let [filter-expr (qf/parse-filter "(and,(eq,appName,web-app),(eq,operationalState,STARTED))")] + (is (= :and (:op filter-expr))) + (is (= 2 (count (:exprs filter-expr)))) + (is (= :eq (:op (first (:exprs filter-expr))))) + (is (= :eq (:op (second (:exprs filter-expr)))))))) + + +(deftest test-parse-filter-or + (testing "Parse OR filter" + (let [filter-expr (qf/parse-filter "(or,(eq,appName,web-app),(eq,appName,database))")] + (is (= :or (:op filter-expr))) + (is (= 2 (count (:exprs filter-expr))))))) + + +(deftest test-parse-filter-empty + (testing "Parse empty filter returns nil" + (is (nil? (qf/parse-filter ""))) + (is (nil? (qf/parse-filter nil))) + (is (nil? (qf/parse-filter " "))))) + + +;; +;; Filter Application Tests +;; + +(deftest test-apply-filter-eq + (testing "Apply equality filter" + (let [filter-expr (qf/parse-filter "(eq,appName,web-app)") + result (qf/apply-filter filter-expr test-resources)] + (is (= 2 (count result))) + (is (every? #(= "web-app" (:appName %)) result))))) + + +(deftest test-apply-filter-neq + (testing "Apply not-equal filter" + (let [filter-expr (qf/parse-filter "(neq,operationalState,STOPPED)") + result (qf/apply-filter filter-expr test-resources)] + (is (= 3 (count result))) + (is (every? #(not= "STOPPED" (:operationalState %)) result))))) + + +(deftest test-apply-filter-gt + (testing "Apply greater-than filter" + (let [filter-expr (qf/parse-filter "(gt,cpu,2)") + result (qf/apply-filter filter-expr test-resources)] + (is (= 2 (count result))) + (is (every? #(> (:cpu %) 2) result))))) + + +(deftest test-apply-filter-lt + (testing "Apply less-than filter" + (let [filter-expr (qf/parse-filter "(lt,memory,8192)") + result (qf/apply-filter filter-expr test-resources)] + (is (= 3 (count result))) + (is (every? #(< (:memory %) 8192) result))))) + + +(deftest test-apply-filter-in + (testing "Apply in-set filter" + (let [filter-expr (qf/parse-filter "(in,appName,web-app,database)") + result (qf/apply-filter filter-expr test-resources)] + (is (= 3 (count result))) + (is (every? #(contains? #{"web-app" "database"} (:appName %)) result))))) + + +(deftest test-apply-filter-and + (testing "Apply AND filter" + (let [filter-expr (qf/parse-filter "(and,(eq,appName,web-app),(eq,operationalState,STARTED))") + result (qf/apply-filter filter-expr test-resources)] + (is (= 2 (count result))) + (is (every? #(and (= "web-app" (:appName %)) + (= "STARTED" (:operationalState %))) + result))))) + + +(deftest test-apply-filter-or + (testing "Apply OR filter" + (let [filter-expr (qf/parse-filter "(or,(eq,appName,web-app),(eq,appName,database))") + result (qf/apply-filter filter-expr test-resources)] + (is (= 3 (count result))) + (is (every? #(or (= "web-app" (:appName %)) + (= "database" (:appName %))) + result))))) + + +(deftest test-apply-filter-complex + (testing "Apply complex nested filter" + (let [filter-expr (qf/parse-filter "(and,(or,(eq,appName,web-app),(eq,appName,database)),(eq,operationalState,STARTED))") + result (qf/apply-filter filter-expr test-resources)] + (is (= 3 (count result))) + (is (every? #(= "STARTED" (:operationalState %)) result))))) + + +(deftest test-apply-filter-nil + (testing "Apply nil filter returns all resources" + (let [result (qf/apply-filter nil test-resources)] + (is (= (count test-resources) (count result)))))) + + +;; +;; Pagination Tests +;; + +(deftest test-paginate-first-page + (testing "Paginate first page" + (let [result (qf/paginate test-resources {:page 1 :size 2 :base-uri "/app_instances"})] + (is (= 2 (count (:items result)))) + (is (= 5 (:total result))) + (is (= 1 (:page result))) + (is (= 2 (:size result))) + (is (= 3 (:totalPages result))) + (is (some? (get-in result [:_links :self]))) + (is (some? (get-in result [:_links :next]))) + (is (nil? (get-in result [:_links :prev])))))) + + +(deftest test-paginate-middle-page + (testing "Paginate middle page" + (let [result (qf/paginate test-resources {:page 2 :size 2 :base-uri "/app_instances"})] + (is (= 2 (count (:items result)))) + (is (= 2 (:page result))) + (is (some? (get-in result [:_links :prev]))) + (is (some? (get-in result [:_links :next]))) + (is (some? (get-in result [:_links :first])))))) + + +(deftest test-paginate-last-page + (testing "Paginate last page" + (let [result (qf/paginate test-resources {:page 3 :size 2 :base-uri "/app_instances"})] + (is (= 1 (count (:items result)))) + (is (= 3 (:page result))) + (is (some? (get-in result [:_links :prev]))) + (is (nil? (get-in result [:_links :next])))))) + + +(deftest test-paginate-size-cap + (testing "Paginate size capped at 100" + (let [result (qf/paginate test-resources {:page 1 :size 200})] + (is (<= (:size result) 100))))) + + +(deftest test-paginate-invalid-page + (testing "Paginate invalid page number defaults to 1" + (let [result (qf/paginate test-resources {:page 0 :size 2})] + (is (= 1 (:page result)))))) + + +;; +;; Field Selection Tests +;; + +(deftest test-parse-fields + (testing "Parse fields parameter" + (let [fields (qf/parse-fields "appName,operationalState,cpu")] + (is (= #{:appName :operationalState :cpu} fields))))) + + +(deftest test-parse-fields-empty + (testing "Parse empty fields returns nil" + (is (nil? (qf/parse-fields ""))) + (is (nil? (qf/parse-fields nil))) + (is (nil? (qf/parse-fields " "))))) + + +(deftest test-select-fields + (testing "Select specific fields from resources" + (let [fields #{:appName :operationalState} + result (qf/select-fields fields test-resources)] + (is (= 5 (count result))) + (is (every? #(contains? % :id) result)) ; :id always included + (is (every? #(contains? % :appName) result)) + (is (every? #(contains? % :operationalState) result)) + (is (every? #(not (contains? % :cpu)) result)) + (is (every? #(not (contains? % :memory)) result))))) + + +(deftest test-select-fields-nil + (testing "Select nil fields returns all fields" + (let [result (qf/select-fields nil test-resources)] + (is (= (count test-resources) (count result))) + (is (every? #(contains? % :cpu) result))))) + + +;; +;; Combined Query Processing Tests +;; + +(deftest test-process-query-filter-only + (testing "Process query with filter only" + (let [result (qf/process-query test-resources {:filter "(eq,appName,web-app)"})] + (is (= 2 (count (:items result)))) + (is (= 2 (:total result)))))) + + +(deftest test-process-query-pagination-only + (testing "Process query with pagination only" + (let [result (qf/process-query test-resources {:page "1" :size "2"})] + (is (= 2 (count (:items result)))) + (is (= 5 (:total result))) + (is (= 1 (:page result)))))) + + +(deftest test-process-query-fields-only + (testing "Process query with field selection only" + (let [result (qf/process-query test-resources {:fields "appName,operationalState"})] + (is (every? #(contains? % :id) (:items result))) + (is (every? #(contains? % :appName) (:items result))) + (is (every? #(not (contains? % :cpu)) (:items result)))))) + + +(deftest test-process-query-combined + (testing "Process query with filter, pagination, and field selection" + (let [result (qf/process-query test-resources + {:filter "(eq,operationalState,STARTED)" + :page "1" + :size "2" + :fields "appName,cpu" + :base-uri "/app_instances"})] + (is (= 2 (count (:items result)))) + (is (= 3 (:total result))) ; 3 STARTED apps total + (is (every? #(contains? % :appName) (:items result))) + (is (every? #(not (contains? % :memory)) (:items result))) + (is (some? (get-in result [:_links :self]))) + (is (some? (get-in result [:_links :next])))))) + + +(deftest test-process-query-empty + (testing "Process query with no parameters" + (let [result (qf/process-query test-resources {})] + (is (= 5 (count (:items result)))) + (is (= 5 (:total result))) + (is (= 1 (:page result))) + (is (= 20 (:size result)))))) + + +;; +;; Module Completeness Test +;; + +(deftest test-query-filter-complete + (testing "Verify all expected functions are available" + (let [expected-fns ['parse-filter + 'apply-filter + 'paginate + 'parse-fields + 'select-fields + 'process-query]] + (doseq [fn-name expected-fns] + (is (some? (ns-resolve 'com.sixsq.nuvla.server.resources.mec.query-filter fn-name)) + (str "Function " fn-name " should be defined")))))) diff --git a/code/test/com/sixsq/nuvla/server/resources/mepm_lifecycle_test.clj b/code/test/com/sixsq/nuvla/server/resources/mepm_lifecycle_test.clj new file mode 100644 index 000000000..13ddf4019 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/mepm_lifecycle_test.clj @@ -0,0 +1,191 @@ +(ns com.sixsq.nuvla.server.resources.mepm-lifecycle-test + (:require + [clojure.test :refer [deftest is use-fixtures]] + [com.sixsq.nuvla.server.app.params :as p] + [com.sixsq.nuvla.server.middleware.authn-info :refer [authn-info-header]] + [com.sixsq.nuvla.server.resources.common.utils :as u] + [com.sixsq.nuvla.server.resources.lifecycle-test-utils :as ltu] + [com.sixsq.nuvla.server.resources.mec.mm5-client :as mm5] + [com.sixsq.nuvla.server.resources.mepm :as mepm] + [jsonista.core :as json] + [peridot.core :refer [content-type header request session]])) + + +(use-fixtures :once ltu/with-test-server-fixture) + + +(def base-uri (str p/service-context mepm/resource-type)) + + +(def valid-mepm + {:name "Test MEPM" + :description "Test MEC Platform Manager" + :endpoint "https://mepm.example.com:8443" + :capabilities {:platforms ["x86_64" "arm64"] + :services ["mec-service-1" "mec-service-2"] + :api-version "v2"} + :resources {:cpu-cores 64 + :memory-gb 256 + :storage-gb 2048 + :gpu-count 8} + :status "ONLINE" + :mec-host-id "mec-host/test-host-123" + :credential-id "credential/test-credential-456" + :version "2.1.0" + :tags ["production" "edge"]}) + + +(deftest lifecycle + ;; Mock Mm5 client responses for testing + (with-redefs [mm5/check-health (fn [_endpoint & [_opts]] + {:success? true + :status 200 + :data {:status "healthy" :uptime-seconds 86400}}) + mm5/query-capabilities (fn [_endpoint & [_opts]] + {:success? true + :status 200 + :data {:platforms ["x86_64" "arm64"] + :services ["mec-service-1" "mec-service-2"] + :api-version "v2"}}) + mm5/query-resources (fn [_endpoint & [_opts]] + {:success? true + :status 200 + :data {:cpu-cores 64 + :memory-gb 256 + :storage-gb 1000 + :gpu-count 2}})] + (let [session-anon (-> (ltu/ring-app) + session + (content-type "application/json")) + session-admin (header session-anon authn-info-header + "group/nuvla-admin group/nuvla-admin group/nuvla-user group/nuvla-anon") + session-user (header session-anon authn-info-header + "user/jane user/jane group/nuvla-user group/nuvla-anon")] + + ;; Anonymous query should fail + (-> session-anon + (request base-uri) + (ltu/body->edn) + (ltu/is-status 403)) + + ;; Admin query should succeed but have no MEPMs initially + (-> session-admin + (request base-uri) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/is-count zero?)) + + ;; User query should succeed + (-> session-user + (request base-uri) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/is-count zero?)) + + ;; Anonymous create should fail + (-> session-anon + (request base-uri + :request-method :post + :body (json/write-value-as-string valid-mepm)) + (ltu/body->edn) + (ltu/is-status 403)) + + ;; User create should succeed + (let [resp (-> session-user + (request base-uri + :request-method :post + :body (json/write-value-as-string valid-mepm)) + (ltu/body->edn) + (ltu/is-status 201)) + id (ltu/body-resource-id resp) + location (ltu/location resp) + uri (str p/service-context id)] + + ;; Check created resource + (let [mepm (-> session-user + (request uri) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/body))] + (is (= "Test MEPM" (:name mepm))) + (is (= "https://mepm.example.com:8443" (:endpoint mepm))) + (is (= "ONLINE" (:status mepm))) + (is (= ["x86_64" "arm64"] (get-in mepm [:capabilities :platforms]))) + (is (= 64 (get-in mepm [:resources :cpu-cores]))) + (is (:created mepm)) + (is (:updated mepm))) + + ;; Update MEPM status + (let [updated-mepm (-> session-user + (request uri + :request-method :put + :body (json/write-value-as-string {:status "DEGRADED"})) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/body))] + (is (= "DEGRADED" (:status updated-mepm)))) + + ;; Check available operations + (let [ops (-> session-user + (request uri) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/body) + :operations)] + (is (some #(= "check-health" (:rel %)) ops)) + (is (some #(= "query-capabilities" (:rel %)) ops)) + (is (some #(= "query-resources" (:rel %)) ops))) + + ;; Test check-health action + (-> session-user + (request (str uri "/check-health") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200)) + + ;; Verify last-check was updated + (let [mepm-after-check (-> session-user + (request uri) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/body))] + (is (:last-check mepm-after-check))) + + ;; Test query-capabilities action + (let [resp (-> session-user + (request (str uri "/query-capabilities") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/body))] + (is (= ["x86_64" "arm64"] (get-in resp [:message :platforms])))) + + ;; Test query-resources action + (let [resp (-> session-user + (request (str uri "/query-resources") + :request-method :post + :body (json/write-value-as-string {})) + (ltu/body->edn) + (ltu/is-status 200) + (ltu/body))] + (is (= 64 (get-in resp [:message :cpu-cores])))) + + ;; Delete MEPM + (-> session-user + (request uri :request-method :delete) + (ltu/body->edn) + (ltu/is-status 200)) + + ;; Verify deletion + (-> session-user + (request uri) + (ltu/body->edn) + (ltu/is-status 404)))))) + + +(deftest bad-methods + (let [resource-uri (str p/service-context (u/new-resource-id mepm/resource-type))] + (ltu/verify-405-status [[base-uri :delete] + [resource-uri :post]]))) diff --git a/code/test/com/sixsq/nuvla/server/resources/module_application_mec_test.clj b/code/test/com/sixsq/nuvla/server/resources/module_application_mec_test.clj new file mode 100644 index 000000000..34bb3a218 --- /dev/null +++ b/code/test/com/sixsq/nuvla/server/resources/module_application_mec_test.clj @@ -0,0 +1,168 @@ +(ns com.sixsq.nuvla.server.resources.module-application-mec-test + "Tests for MEC 037 Application Descriptor module subtype" + (:require + [clojure.test :refer [deftest is testing use-fixtures]] + [com.sixsq.nuvla.server.resources.module-application-mec :as t] + [com.sixsq.nuvla.server.resources.spec.module-application-mec :as spec-mec] + [clojure.spec.alpha :as s])) + + +(deftest check-subtype-constant + (testing "Subtype constant matches spec" + (is (= "application_mec" t/subtype)) + (is (= "application_mec" spec-mec/subtype)))) + + +(deftest check-resource-type + (testing "Resource type is correctly formed" + (is (= "module-application-mec" t/resource-type)))) + + +(deftest test-validate-appd-content + (testing "Valid MEC AppD content passes validation" + (let [valid-appd {:appDId "module/test-app-123" + :appDVersion "1.0" + :appName "Test MEC App" + :appProvider "Test Provider" + :appSoftVersion "1.0.0" + :mecVersion "3.1.1" + :virtualComputeDescriptor {:virtualCpu {:numVirtualCpu 2} + :virtualMemory {:virtualMemSize 2048}} + :swImageDescriptor [{:swImageName "test-image" + :swImageVersion "1.0" + :containerFormat :DOCKER + :swImage "registry.example.com/test:1.0" + :minDisk 1 + :minRam 512}]}] + (is (= valid-appd (t/validate-appd-content valid-appd))))) + + (testing "Invalid MEC AppD content throws exception" + (let [invalid-appd {:appName "Test" ;; Missing required fields + :appProvider "Provider"}] + (is (thrown? Exception (t/validate-appd-content invalid-appd)))))) + + +(deftest test-validate-container-images + (testing "Valid container image passes validation" + (let [content {:swImageDescriptor [{:swImageName "test" + :swImageVersion "1.0" + :containerFormat :DOCKER + :swImage "registry.example.com/app:1.0" + :minDisk 1 + :minRam 256}]}] + (is (= content (t/validate-container-images content))))) + + (testing "Missing images throws exception" + (let [content {:swImageDescriptor []}] + (is (thrown? Exception (t/validate-container-images content))))) + + (testing "Invalid image format throws exception" + (let [content {:swImageDescriptor [{:swImageName "test" + :swImageVersion "1.0" + :containerFormat :DOCKER + :swImage "INVALID FORMAT!" + :minDisk 1 + :minRam 256}]}] + (is (thrown? Exception (t/validate-container-images content)))))) + + +(deftest test-extract-resource-summary + (testing "Resource summary extraction" + (let [content {:mecVersion "3.1.1" + :virtualComputeDescriptor {:virtualCpu {:numVirtualCpu 4} + :virtualMemory {:virtualMemSize 8192}} + :virtualStorageDescriptor [{:sizeOfStorage 100} + {:sizeOfStorage 50}] + :swImageDescriptor [{:swImageName "test1"} + {:swImageName "test2"}] + :appServiceRequired [{:serName :rnis} + {:serName :location}]} + summary (t/extract-resource-summary content)] + (is (= 4 (:cpus summary))) + (is (= 8192 (:memory-mb summary))) + (is (= 150 (:storage-gb summary))) + (is (= 2 (:images summary))) + (is (= "3.1.1" (:mec-version summary))) + (is (= [:rnis :location] (:requires-mec-services summary)))))) + + +(deftest test-extract-deployment-info + (testing "Deployment info extraction" + (let [content {:appName "My App" + :appSoftVersion "1.2.3" + :appProvider "Acme Corp" + :appDescription "Test application" + :mecVersion "3.1.1" + :virtualComputeDescriptor {:virtualCpu {:numVirtualCpu 2} + :virtualMemory {:virtualMemSize 4096}} + :virtualStorageDescriptor [{:sizeOfStorage 50}] + :swImageDescriptor [{:swImageName "app" + :swImageVersion "1.2.3" + :swImage "registry.io/app:1.2.3" + :containerFormat :DOCKER}] + :appExtCpd [{:cpdId "eth0"}] + :trafficRuleDescriptor [{:trafficRuleId "rule1"}] + :dnsRuleDescriptor [{:dnsRuleId "dns1"}] + :appServiceRequired [{:serName :rnis :version "2.1.1"}]} + info (t/extract-deployment-info content)] + (is (= "My App" (:app-name info))) + (is (= "1.2.3" (:app-version info))) + (is (= "Acme Corp" (:provider info))) + (is (= 1 (count (:container-images info)))) + (is (= 2 (get-in info [:resource-requirements :cpus]))) + (is (= 1 (get-in info [:network-requirements :external-connections]))) + (is (= 1 (count (:mec-services info))))))) + + +(deftest test-appd-to-deployment-params + (testing "AppD to deployment params conversion" + (let [content {:appName "Test App" + :appProvider "Provider" + :appSoftVersion "1.0" + :virtualComputeDescriptor {:virtualCpu {:numVirtualCpu 2}} + :swImageDescriptor [{:swImage "registry.io/test:1.0" + :containerFormat :DOCKER}] + :appServiceRequired [{:serName :rnis :version "2.1.1"}] + :trafficRuleDescriptor [] + :dnsRuleDescriptor []} + params (t/appd->deployment-params "module/test-123" content)] + (is (= "module/test-123" (:appDId params))) + (is (= "Test App" (:appName params))) + (is (= "registry.io/test:1.0" (:containerImage params))) + (is (= "DOCKER" (:containerFormat params))) + (is (= 1 (count (:appServiceRequired params))))))) + + +(deftest test-check-mec-compatibility + (testing "Compatible MEC AppD" + (let [content {:mecVersion "3.1.1" + :appServiceRequired [{:serName :rnis} + {:serName :location}]} + capabilities {:mecVersion "3.2.1" + :availableServices [:rnis :location :bandwidth-management]} + result (t/check-mec-compatibility content capabilities)] + (is (:compatible? result)) + (is (:version-match? result)) + (is (:services-match? result)) + (is (empty? (:missing-services result))))) + + (testing "Incompatible - missing services" + (let [content {:mecVersion "3.1.1" + :appServiceRequired [{:serName :rnis} + {:serName :ue-identity}]} + capabilities {:mecVersion "3.2.1" + :availableServices [:rnis :location]} + result (t/check-mec-compatibility content capabilities)] + (is (not (:compatible? result))) + (is (:version-match? result)) + (is (not (:services-match? result))) + (is (= #{:ue-identity} (:missing-services result))))) + + (testing "Incompatible - old MEC version" + (let [content {:mecVersion "4.0.0" + :appServiceRequired [{:serName :rnis}]} + capabilities {:mecVersion "3.2.1" + :availableServices [:rnis :location]} + result (t/check-mec-compatibility content capabilities)] + (is (not (:compatible? result))) + (is (not (:version-match? result)))))) diff --git a/docs/5g-emerge/3GPP/compliance-study-final.md b/docs/5g-emerge/3GPP/compliance-study-final.md new file mode 100644 index 000000000..94a3dc2c7 --- /dev/null +++ b/docs/5g-emerge/3GPP/compliance-study-final.md @@ -0,0 +1,27 @@ +# 3.2 Assessment of Compliance with 3GPP + +## 3.2.1 3GPP Management Architecture and Nuvla Positioning + +The 3GPP Edge Computing Management architecture, specified in **3GPP TS 28.538**, defines the requirements for lifecycle management, performance assurance, and fault supervision of edge components such as Edge Application Servers (EAS) and Edge Enabler Servers (EES). This specification supports flexible deployment models where the "ECSP Management System" (Edge Computing Service Provider) can leverage underlying orchestration frameworks to perform the actual resource and application lifecycle management. + +Crucially, 3GPP TS 28.538 explicitly identifies ETSI MEC as a valid realization of the Edge Hosting Environment management layer, where a MEC Orchestrator fulfills the resource and application lifecycle management responsibilities. In this context, **Nuvla is positioned as the MEC Orchestrator (MEO)** within the 3GPP management framework. By acting as the MEO, Nuvla provides the necessary application lifecycle management (LCM) capabilities (instantiation, termination, updates) required by the 3GPP management system, effectively acting as the functional backend for 3GPP edge deployments. + +## 3.2.2 Gap Analysis vs. 3GPP Management Responsibilities + +While Nuvla fulfils the functional requirements for orchestrating 3GPP edge workloads, a gap analysis against the strict 3GPP TS 28.538 management interfaces reveals the following: + +### 3GPP Network Resource Model (NRM) Support (Gap) + +3GPP TS 28.538 defines a specific Network Resource Model (NRM) and Management Services (MnS) exposed via RESTful HTTP interfaces for creating and managing MOIs (Managed Object Instances). + +- **Current Status:** Nuvla does not natively implement the 3GPP NRM or the specific MnS northbound interface. + +- **Strategic Mitigation:** Implementation of the full 3GPP NRM is resource-intensive and potentially redundant for this implementation context. Instead, Nuvla adopts a strategy where 3GPP compliance is achieved via the ETSI MEC interface. An external "Translation" or "Proxy" layer (acting as the ECSP Management System) could theoretically translate 3GPP NRM intent into Nuvla/MEC commands, but for the scope of WP2.3.2, Nuvla's MEC compliance is considered sufficient to support 3GPP use cases. + +## Conclusion on 3GPP Compliance + +Nuvla achieves **practical compliance** with 3GPP TS 28.538 through the standards-recognized architecture option of implementing the Edge Hosting Environment management via ETSI MEC. By providing full MEC MEO capabilities, Nuvla fulfils the orchestration and lifecycle management responsibilities expected by the ECSP Management System in the 3GPP architecture. + +While Nuvla does not natively implement the 3GPP NRM or MnS interfaces, these are optional in deployments that realize the TS 28.538 framework through ETSI MEC. Accordingly, Nuvla provides **functional compliance** with 3GPP requirements for edge workload management without requiring native adoption of 3GPP-specific management models. + + diff --git a/docs/5g-emerge/3GPP/compliance-study.md b/docs/5g-emerge/3GPP/compliance-study.md new file mode 100644 index 000000000..67e07c8ac --- /dev/null +++ b/docs/5g-emerge/3GPP/compliance-study.md @@ -0,0 +1,624 @@ +# Compliance Study of Nuvla Edge Orchestration Solution +## Assessment of Compliance with 3GPP Edge Computing Standards + +**Version:** 1.0 +**Date:** October 2025 +**Standards:** 3GPP TS 23.558 v19.6.0, TS 23.501 v19.5.0, TS 28.538 v17.4.0 +**Scope:** Edge Application Enablement and Management + +--- + +## Executive Summary + +This document assesses Nuvla's compliance with **3GPP edge computing standards** for operating as an **Edge Application Management system** integrated with 5G networks. Unlike ETSI MEC which focuses on MEC Orchestrator (MEO) functionality, 3GPP defines edge computing through the **Edge Application Enablement Layer (EDGEAPP)** with complementary interfaces to the 5G System. + +**Key 3GPP Edge Components:** +- **Edge Enabler Server (EES)**: Discovers, authorizes, and manages edge applications +- **Edge Configuration Server (ECS)**: Provisions and configures edge application clients +- **Edge Application Server (EAS)**: Hosts edge applications at network edge +- **Application Function (AF)**: Interfaces with 5G Core for network services + +**Target:** Minimum Viable Edge Enablement Platform (Phase 1 + Phase 2) - integration-ready with 5G networks. + +**Key Finding:** Nuvla has strong foundational capabilities but requires significant extensions to support 3GPP-specific edge enablement APIs, 5G Core integration, and standardized edge discovery/provisioning mechanisms. + +--- + +## 1. 3GPP Edge Computing Architecture Overview + +### 1.1 Architecture Components (TS 23.558 §4.2) + +3GPP defines edge computing through an **Enablement Layer** that sits between applications and the 5G System: + +``` +┌──────────────────────────────────────────────────────────┐ +│ Application Layer │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ AC (App │ │ AC (App │ │ AC (App │ │ +│ │ Client) │ │ Client) │ │ Client) │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +└─────────┼─────────────────┼─────────────────┼────────────┘ + │ EDGE-1 │ EDGE-1 │ EDGE-1 + │ │ │ +┌─────────▼─────────────────▼─────────────────▼────────────┐ +│ Edge Application Enablement Layer │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ ECS │ │ EES │ │ EAS │ │ +│ │ (Config) │ │ (Enabler) │ │ (App Host) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +└─────────┼──────────────────┼──────────────────┼───────────┘ + │ EES-ECS │ Nees_Application │ + │ │ │ + │ ▼ Nnef ▼ N33 +┌─────────▼──────────────────────────────────────────────┐ +│ 5G System (5GC) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ NEF │ │ AF │ │ PCF │ │ +│ │(Exposure)│ │(App Func)│ │ (Policy) │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +└────────────────────────────────────────────────────────┘ +``` + +**Key Interfaces:** +- **EDGE-1**: Application Client (AC) ↔ EES/ECS (edge discovery, configuration) +- **EDGE-2**: EES ↔ EAS (application enablement, lifecycle) +- **EDGE-3**: EAS ↔ Application Client (application data) +- **EES-ECS**: EES ↔ ECS (coordination) +- **Nnef**: EES ↔ NEF (5G Core exposure, network capabilities) +- **Naf**: EAS/AF ↔ 5GC (policy, QoS, charging) + +### 1.2 Key Functional Entities + +**Edge Enabler Server (EES) - TS 23.558 §6.2:** +- Edge application discovery and selection +- Edge application context and configuration provisioning +- Application Client (AC) registration and authorization +- Dynamic DNS (ADRF - Access Traffic Routing Function) +- Service provisioning information to UEs +- Integration with 5G Core via NEF + +**Edge Configuration Server (ECS) - TS 23.558 §6.3:** +- Edge application client profile management +- Configuration provisioning to application clients +- Application Context Transfer (ACT) coordination +- Service continuity support + +**Edge Application Server (EAS) - TS 23.558 §6.4:** +- Hosts edge applications +- Provides edge services to application clients +- Dynamic relocation based on UE mobility +- Integration with 5G Core for QoS, policies + +**Application Function (AF) - TS 23.501 §6.2.6:** +- Interacts with 5G Core (PCF, NEF, UDM) +- Requests network services (QoS, exposure, analytics) +- Subscribes to events (mobility, location, QoS changes) + +--- + +## 2. 3GPP Edge Requirements for Nuvla + +### Priority 1: Critical (Minimum Viable Edge Platform) + +#### R1 - Edge Enabler Server (EES) Functionality (TS 23.558 §7.2) + +**Core Requirements:** +- **Application Discovery (EDGE-1)**: REST API for AC to discover available edge applications + - Input: Application ID, location, service area + - Output: EAS endpoint(s), service details, service area info + - Support for DNS-based and API-based discovery +- **Application Context Provisioning**: Provide application-specific configuration to ACs +- **EAS Selection**: Select optimal EAS based on location, load, capabilities +- **Service Continuity**: Track AC location changes, trigger EAS relocation when needed +- **Dynamic DNS (ADRF)**: Resolve application FQDNs to edge-optimal EAS IPs + +**Data Models (TS 23.558 Annex B):** +- `EdgeAppInfo`: Application ID, name, version, service area, EAS endpoints +- `ACProfile`: AC identifier, location, subscribed applications +- `ServiceProvisioningInfo`: EAS endpoint, DNS configuration, context data + +**APIs Required (6 endpoints):** +1. `POST /ees/discovery` - Discover edge applications +2. `GET /ees/applications/{appId}` - Get application details +3. `POST /ees/context-provisioning` - Provision AC context +4. `GET /ees/service-area` - Query service area information +5. `POST /ees/dns-resolution` - Dynamic DNS resolution +6. `POST /ees/subscriptions` - Subscribe to EAS changes/events + +**Effort:** 6-8 weeks (new functionality, 5G integration complexity) + +--- + +#### R2 - Edge Configuration Server (ECS) Functionality (TS 23.558 §7.3) + +**Core Requirements:** +- **Configuration Management**: Store and provide AC profiles and configurations +- **Application Context Transfer (ACT)**: Coordinate state transfer between EAS instances +- **AC Registration**: Register application clients with authorized configurations +- **Configuration Updates**: Push configuration updates to registered ACs + +**Data Models:** +- `ACConfiguration`: AC profile, allowed EAS, policies, QoS parameters +- `ACTInfo`: Transfer state, source EAS, target EAS, context data + +**APIs Required (5 endpoints):** +1. `POST /ecs/registration` - Register application client +2. `GET /ecs/configuration/{acId}` - Retrieve AC configuration +3. `PUT /ecs/configuration/{acId}` - Update AC configuration +4. `POST /ecs/act-initiate` - Initiate application context transfer +5. `GET /ecs/act-status/{actId}` - Query ACT status + +**Effort:** 4-5 weeks (moderate complexity, state management) + +--- + +#### R3 - 5G Core Integration via NEF (TS 23.501 §5.6, TS 29.522) + +**Core Requirements:** +- **NEF Client**: HTTP/2 REST client to interact with 5G Network Exposure Function +- **Service Discovery**: Query 5G network capabilities (analytics, QoS, events) +- **Event Subscriptions**: Subscribe to UE mobility, location, QoS monitoring events +- **Application QoS**: Request application-level QoS guarantees for edge traffic +- **Analytics Consumption**: Retrieve network analytics (UE mobility predictions, load) + +**NEF APIs (TS 29.522 - Selected):** +- `Nnef_EventExposure`: Subscribe to network events (UE mobility, location changes) +- `Nnef_AnalyticsExposure`: Consume network analytics +- `Nnef_PFDManagement`: Manage Packet Flow Descriptions +- `Nnef_TrafficInfluence`: Request traffic steering to edge + +**Operations Required:** +1. NEF authentication (OAuth 2.0) +2. Event subscription creation/deletion +3. Event notification webhook handling +4. Analytics query execution +5. Traffic influence requests + +**Effort:** 5-7 weeks (complex, requires 5G Core access, security) + +--- + +#### R4 - Application Function (AF) Integration (TS 23.501 §5.6.7) + +**Core Requirements:** +- **Policy Coordination (N5 Interface)**: Request policies from PCF for application sessions +- **QoS Flow Management**: Request guaranteed QoS for edge application traffic +- **Charging Correlation**: Provide charging identifiers for application usage +- **Service Data Flow (SDF) Management**: Define traffic filters for edge applications + +**AF APIs (TS 29.514, TS 29.522):** +- `Npcf_PolicyAuthorization`: Request application session policies +- `Naf_EventExposure`: Subscribe to application session events +- Session establishment/modification/termination + +**Effort:** 4-6 weeks (moderate complexity, requires PCF connectivity) + +--- + +### Priority 2: Important (Production Edge Platform) + +#### R5 - Application Lifecycle Management for EAS (TS 28.538) + +**Core Requirements:** +- **EAS Deployment**: Deploy, scale, terminate EAS instances dynamically +- **EAS Monitoring**: Health checks, resource utilization, performance metrics +- **EAS Discovery Registry**: Maintain registry of available EAS instances +- **Multi-Site Orchestration**: Coordinate EAS across multiple edge sites + +**Operations:** +- Deploy EAS (container/VM) to edge compute nodes +- Scale EAS horizontally based on load +- Migrate EAS across edge sites for service continuity +- Decommission EAS instances + +**Effort:** 5-6 weeks (builds on existing Nuvla deployment capabilities) + +--- + +#### R6 - Service Continuity and Application Context Transfer (TS 23.558 §7.5) + +**Core Requirements:** +- **Seamless Handover**: Transfer application state when AC moves between EAS +- **Context State Management**: Serialize/deserialize application state +- **ACT Coordination**: EES-initiated transfer between source and target EAS +- **Failure Handling**: Rollback, retry logic for failed transfers + +**ACT Flow:** +1. EES detects AC location change (via NEF event) +2. EES selects new target EAS closer to AC +3. EES initiates ACT with source and target EAS +4. Source EAS serializes application state +5. Target EAS receives state, prepares to serve AC +6. EES updates AC with new EAS endpoint + +**Effort:** 6-8 weeks (complex, requires state management, coordination) + +--- + +#### R7 - Edge Analytics and Telemetry (TS 23.558 §7.7) + +**Core Requirements:** +- **EAS Performance Metrics**: Latency, throughput, resource usage +- **Application Analytics**: Usage patterns, active sessions, request counts +- **Network Analytics Integration**: Consume 5G network analytics from NEF/NWDAF +- **Predictive Optimization**: Use analytics for proactive EAS selection/relocation + +**Metrics to Collect:** +- EAS response time (P50, P95, P99) +- Active application sessions +- Resource utilization (CPU, memory, network) +- UE-to-EAS distance/latency +- Context transfer success rate + +**Effort:** 3-4 weeks (moderate, telemetry infrastructure) + +--- + +#### R8 - Edge Security and Authentication (TS 33.558) + +**Core Requirements:** +- **AC Authentication**: OAuth 2.0 / OIDC for application client authentication +- **API Security (API-GW)**: TLS 1.3, mTLS for EES/ECS APIs +- **Authorization Policies**: Role-based access control for edge applications +- **Token Management**: JWT issuance, validation, refresh + +**Security Features:** +- TLS 1.3 for all EDGE-1, EDGE-2, NEF interfaces +- Mutual TLS (mTLS) for service-to-service communication +- OAuth 2.0 authorization flows +- API rate limiting and DDoS protection + +**Effort:** 4-5 weeks (security hardening, OAuth/OIDC integration) + +--- + +### Priority 3: Advanced (Future Enhancements) + +**R9 - Multi-Access Edge Computing (MEC + 3GPP Convergence)**: Interoperability between ETSI MEC and 3GPP EDGEAPP (6-8 weeks) + +**R10 - Network Slicing Integration**: Deploy edge applications within specific 5G network slices (5-6 weeks) + +**R11 - AI/ML-Driven EAS Placement**: Use ML models for intelligent EAS selection and relocation (8-10 weeks) + +**R12 - Multi-Operator Scenarios**: Support roaming, multi-operator edge access (6-8 weeks) + +--- + +## 3. Gap Analysis + +Nuvla has strong foundational capabilities but **significant gaps** in 3GPP-specific edge enablement: + +### Critical Gaps (Block 3GPP Compliance) + +| Gap | Requirement | Current State | Effort | Priority | +|-----|-------------|---------------|--------|----------| +| **Gap 1** | EES APIs (6 endpoints, discovery, provisioning) | ❌ Not implemented | 6-8 weeks | 🔴 CRITICAL | +| **Gap 2** | ECS APIs (5 endpoints, config, ACT) | ❌ Not implemented | 4-5 weeks | 🔴 CRITICAL | +| **Gap 3** | NEF Integration (event subscriptions, analytics) | ❌ No 5G Core connectivity | 5-7 weeks | 🔴 CRITICAL | +| **Gap 4** | AF Integration (policy, QoS requests) | ❌ No AF functionality | 4-6 weeks | 🔴 CRITICAL | + +**Phase 1 Total:** 19-26 weeks + +### Important Gaps (Limit Production Use) + +| Gap | Requirement | Current State | Effort | Priority | +|-----|-------------|---------------|--------|----------| +| **Gap 5** | EAS Lifecycle (deploy, monitor, scale) | ✅ Partial (Nuvla deployments) | 5-6 weeks | 🟡 HIGH | +| **Gap 6** | Service Continuity (ACT, handover) | ❌ No state transfer | 6-8 weeks | 🟡 HIGH | +| **Gap 7** | Edge Analytics (metrics, telemetry) | ✅ Partial (Nuvla metrics) | 3-4 weeks | 🟡 MEDIUM | +| **Gap 8** | Security (OAuth, mTLS, API-GW) | ✅ Partial (Nuvla ACL, auth) | 4-5 weeks | 🟡 MEDIUM | + +**Phase 2 Total:** 18-23 weeks + +### Existing Nuvla Capabilities (Leverage) + +| Capability | Relevance to 3GPP | Adaptation Needed | +|------------|-------------------|-------------------| +| **Module Resources** | Package management for EAS | ✅ Map to EdgeAppInfo | +| **Deployment Resources** | EAS deployment, lifecycle | ✅ Extend with EAS-specific logic | +| **NuvlaEdge Resources** | Edge host inventory | ✅ Integrate with EAS registry | +| **Job System** | Operation tracking | ✅ Track ACT operations | +| **Event System** | Notifications | ✅ Map to NEF event subscriptions | +| **ACL System** | Multi-tenancy, RBAC | ✅ Extend with OAuth/OIDC | +| **Metrics & Telemetry** | Resource monitoring | ✅ Add EAS performance metrics | + +--- + +## 4. Implementation Roadmap + +### Phase 1: Core 3GPP Edge Enablement (19-26 weeks) + +**Goal:** Implement EES, ECS, and basic 5G Core integration + +**Deliverables:** +1. **EES Implementation (6-8 weeks)** + - 6 REST API endpoints (discovery, provisioning, DNS, subscriptions) + - Data models: EdgeAppInfo, ACProfile, ServiceProvisioningInfo + - EAS selection algorithm (location-based, basic load balancing) + - 50+ unit tests, 10+ integration tests + +2. **ECS Implementation (4-5 weeks)** + - 5 REST API endpoints (registration, configuration, ACT) + - Data models: ACConfiguration, ACTInfo + - AC profile management, configuration provisioning + - 40+ tests + +3. **NEF Integration (5-7 weeks)** + - HTTP/2 client for NEF APIs (TS 29.522) + - Event subscription: UE mobility, location, QoS monitoring + - Webhook handling for NEF notifications + - OAuth 2.0 authentication + - 30+ tests, NEF simulator for testing + +4. **AF Integration (4-6 weeks)** + - Policy coordination with PCF (Npcf_PolicyAuthorization) + - QoS flow requests + - Charging correlation + - 25+ tests, PCF simulator + +**Success Criteria:** +- ✅ EES: 6 endpoints operational, edge application discovery working +- ✅ ECS: 5 endpoints operational, AC registration and configuration working +- ✅ NEF: Event subscriptions functional, receive UE mobility notifications +- ✅ AF: Policy requests successful, QoS flow established +- ✅ 145+ tests passing, 75%+ coverage +- ✅ End-to-end flow: AC discovers EAS via EES, receives configuration from ECS + +--- + +### Phase 2: Production 3GPP Edge Platform (18-23 weeks) + +**Goal:** Add lifecycle management, service continuity, analytics, security + +**Deliverables:** +1. **EAS Lifecycle Management (5-6 weeks)** + - Deploy EAS as containers to edge sites + - Monitor EAS health, performance, resource usage + - Scale EAS dynamically based on load + - EAS registry with discovery API + - 40+ tests + +2. **Service Continuity & ACT (6-8 weeks)** + - Application Context Transfer coordination + - State serialization/deserialization framework + - EES-initiated handover based on UE mobility + - Failure handling, rollback, retries + - 50+ tests + +3. **Edge Analytics & Telemetry (3-4 weeks)** + - Collect EAS metrics (latency, throughput, resource usage) + - Integrate with 5G network analytics (NEF/NWDAF) + - Predictive EAS selection using analytics + - Grafana dashboards for edge telemetry + - 20+ tests + +4. **Security Hardening (4-5 weeks)** + - OAuth 2.0 / OIDC for AC authentication + - mTLS for service-to-service communication + - API Gateway with rate limiting, DDoS protection + - JWT token management + - Security audit, penetration testing + - 30+ tests + +**Success Criteria:** +- ✅ EAS deployed dynamically to edge sites, scaled based on load +- ✅ ACT functional: application state transferred during handover +- ✅ Analytics: EAS performance metrics collected, predictive placement working +- ✅ Security: OAuth/OIDC authentication, mTLS enabled +- ✅ 140+ tests passing, 80%+ coverage +- ✅ End-to-end: AC registers, discovers EAS, receives optimized endpoint, seamless handover on mobility + +--- + +### Phase 3: Advanced Features (Future, 6-12 months) + +**Scope:** MEC-3GPP convergence, network slicing, AI/ML placement, multi-operator support - **Deferred** + +--- + +## 5. 3GPP Certification & Validation + +**Validation Requirements:** +1. **Functional Testing**: All EES, ECS APIs functional per TS 23.558 +2. **5G Core Integration Testing**: NEF, AF interfaces operational with live 5GC or simulator +3. **Interoperability Testing**: EES/ECS interop with 3rd-party EAS, ACs +4. **Performance Testing**: EAS discovery <100ms, ACT <500ms, support 1000+ concurrent ACs +5. **Security Testing**: OAuth/OIDC flows, mTLS handshake, API security audit + +**Certification Path:** +- 3GPP does not have formal certification like ETSI MEC +- Validation via **conformance testing** against 3GPP Test Specifications (TS 34.xxx, TS 36.xxx) +- Participation in **3GPP Plugfests** for interoperability validation +- Operator acceptance testing with live 5G networks + +**Timeline:** Validation phase: 4-6 weeks (after Phase 1 + 2 complete) + +--- + +## 6. Resource Requirements + +### Development (Phase 1 + 2) + +**Team:** +- 2-3 senior developers (full-time, expertise in 5G, edge, cloud-native) +- 0.5 QA engineer (test automation, 5G testing) +- 0.3 technical writer (API docs, integration guides) +- 0.3 DevOps engineer (5G Core simulator, CI/CD) + +**Effort:** 37-49 person-weeks + +**Budget:** €259,000 - €343,000 (development) + €2,500/month (infrastructure: 5G Core simulator, edge sites) + +### Infrastructure + +**Development:** +- 5G Core Network Simulator (Open5GS, free5GC) - for NEF/AF testing +- Edge compute nodes (3-5 VMs/containers) - for EAS deployment +- Test UE simulators - for mobility/handover testing +- CI/CD pipeline - automated testing + +**Production:** +- 5G Core integration (NEF, AF connectivity) - provided by operator +- Edge sites (multi-site deployment) - existing infrastructure +- OAuth/OIDC provider (Keycloak, Auth0) - authentication +- Monitoring stack (Prometheus, Grafana) - telemetry + +--- + +## 7. Risk Assessment + +| Risk | Impact | Probability | Mitigation | +|------|--------|-------------|------------| +| 5G Core access unavailable | High | Medium | Use Open5GS/free5GC simulators for dev/test | +| NEF/AF interface changes | Medium | Low | Follow 3GPP Rel-17/18 specs, version compatibility | +| Complexity underestimated | High | Medium | 25% buffer, phased approach, weekly reviews | +| ACT state transfer complexity | High | Medium | Start with stateless apps, add stateful later | +| Multi-operator scenarios | Medium | Low | Defer to Phase 3, focus on single operator | +| Limited 3GPP expertise | High | Medium | Training, 3GPP spec study, consultant support | + +--- + +## 8. Success Metrics + +**Technical:** +- 90%+ 3GPP TS 23.558 compliance (EES, ECS APIs) +- 80%+ test coverage, 185+ unit tests, 20+ integration tests +- <100ms EAS discovery latency (P95) +- <500ms Application Context Transfer time (P95) +- 99.9% EES/ECS API uptime + +**Business:** +- Integration with 1+ live 5G network operator +- Deployment in 5G-EMERGE project with 5G Core connectivity +- Participation in 3GPP Plugfest (interoperability validation) +- Public documentation and integration guides + +--- + +## 9. Comparison: 3GPP vs. ETSI MEC + +| Aspect | 3GPP EDGEAPP | ETSI MEC | Nuvla Strategy | +|--------|--------------|----------|----------------| +| **Focus** | 5G-native edge enablement | Telco-agnostic edge orchestration | **Both**: MEC for general edge, 3GPP for 5G | +| **Key Component** | EES (Enabler Server) | MEO (Orchestrator) | Implement EES + MEO | +| **5G Integration** | Native (NEF, AF) | Optional (via Mm5) | 3GPP for tight 5G integration | +| **Application Model** | Edge Application Server (EAS) | MEC Application | Unified: EAS = MEC App | +| **Discovery** | EES API (EDGE-1) | MEC Catalog | Dual: EES for 5G, Catalog for MEC | +| **Lifecycle** | EES + ECS | MEO + MEPM | Converged lifecycle mgmt | +| **Service Continuity** | ACT (App Context Transfer) | App relocation | Implement both mechanisms | +| **Standardization** | 3GPP (TS 23.558) | ETSI (GS MEC 003, 010-2) | Comply with both | +| **Certification** | Conformance testing | MECwiki registration | Dual certification path | + +**Recommendation:** Implement **both** standards for maximum market reach: +- **ETSI MEC** for general edge orchestration, operator-agnostic deployments +- **3GPP EDGEAPP** for 5G-native integration, tight coupling with 5G Core +- **Converged Architecture**: Single Nuvla platform with dual interfaces (MEO + EES/ECS) + +--- + +## 10. Conclusion + +Achieving 3GPP edge computing compliance requires **37-49 weeks** of development (Phase 1 + 2) to implement EES, ECS, and 5G Core integration (NEF, AF). + +**Critical Path:** EES Implementation → ECS Implementation → NEF Integration → AF Integration → Service Continuity + +**Key Challenges:** +1. **5G Core Complexity**: NEF, AF integration requires deep 5G knowledge, access to 5GC +2. **Application Context Transfer**: Stateful handover is complex, requires coordination +3. **Security**: OAuth/OIDC, mTLS, API security add significant effort +4. **Dual Standards**: Supporting both ETSI MEC and 3GPP increases scope + +**Recommendation:** Execute **Phase 1 + 2 together** (37-49 weeks total) for production-ready 3GPP edge platform suitable for 5G-EMERGE deployment and operator validation. + +**Strategic Positioning:** +- **Short-term (Phase 1)**: Focus on ETSI MEC compliance for MECwiki registration (10-14 weeks) +- **Medium-term (Phase 2)**: Add 3GPP EDGEAPP support for 5G integration (37-49 weeks total) +- **Long-term (Phase 3)**: Converged MEC+3GPP platform with advanced features (12-18 months) + +**Next Steps:** +1. **Secure 2-3 senior developers** with 5G + edge expertise +2. **Phase 1 kickoff**: EES/ECS architecture design (weeks 1-2) +3. **5G Core setup**: Deploy Open5GS/free5GC simulator (week 2) +4. **EES implementation**: Discovery, provisioning APIs (weeks 3-10) +5. **ECS implementation**: Configuration, ACT APIs (weeks 11-15) +6. **NEF integration**: Event subscriptions, analytics (weeks 16-22) +7. **AF integration**: Policy, QoS coordination (weeks 23-28) +8. **Phase 2 execution**: Lifecycle, service continuity, analytics, security (weeks 29-49) +9. **Validation & testing**: Conformance testing, operator trials (weeks 50-54) +10. **Production deployment**: 5G-EMERGE integration (week 55+) + +--- + +## Appendix A: 3GPP Specifications Reference + +**Core Specifications:** +- **TS 23.558** v19.6.0: Architecture for enabling Edge Applications (EDGEAPP) +- **TS 23.501** v19.5.0: System architecture for the 5G System (5GS) +- **TS 23.502** v19.5.0: Procedures for the 5G System (5GS) +- **TS 28.538** v17.4.0: Management and orchestration of edge computing +- **TS 29.522** v19.3.0: Network Exposure Function (NEF) Northbound APIs +- **TS 29.514** v19.5.0: Policy and Charging Control (Npcf) APIs +- **TS 33.558** v17.5.0: Security aspects of edge computing + +**Supporting Specifications:** +- **TS 23.503**: Policy and charging control framework for 5GS +- **TS 23.288**: Architecture enhancements for network data analytics (NWDAF) +- **TS 29.122**: T8 interface (SCEF) - precursor to NEF +- **TS 29.551**: Nnef_ServiceDiscovery API +- **TS 34.229**: Conformance testing for edge applications + +--- + +## Appendix B: Existing Nuvla Capabilities Mapping + +| Nuvla Component | 3GPP Equivalent | Adaptation Required | +|-----------------|-----------------|---------------------| +| **Module Resource** | EdgeAppInfo | Add service area, EAS endpoints, 3GPP metadata | +| **Deployment Resource** | EAS Instance | Add ACT support, 5G QoS policies, state mgmt | +| **NuvlaEdge Resource** | Edge Compute Node | Add EAS registry, 5G site info, UPF proximity | +| **Job Resource** | Operation Tracking | Map to ACT operations, NEF event handling | +| **Event Resource** | NEF Notifications | Integrate NEF webhooks, UE mobility events | +| **ACL Resource** | OAuth/OIDC | Extend with OAuth 2.0, JWT tokens | +| **Credential Resource** | API Keys/Tokens | Add NEF credentials, AF authentication | +| **Infrastructure Service** | EES/ECS | Implement new EES/ECS resources with APIs | + +**Strategy:** Extend Nuvla resources with 3GPP-specific fields and logic, implement EES/ECS as new resource types. + +--- + +## Appendix C: 5G Core Simulator Options + +For development and testing without live 5G network: + +1. **Open5GS** (Open Source) + - Full 5G Core implementation (AMF, SMF, UPF, PCF, NEF, etc.) + - Best for: Complete 5G Core testing + - Complexity: High (requires networking knowledge) + - Cost: Free + +2. **free5GC** (Open Source) + - Lightweight 5G Core in Go + - Best for: Quick setup, NEF/AF testing + - Complexity: Medium + - Cost: Free + +3. **UERANSIM** (Open Source) + - 5G UE and RAN simulator + - Best for: Testing UE mobility, handover scenarios + - Complexity: Low + - Cost: Free + +4. **Amarisoft** (Commercial) + - Professional 5G Core + RAN simulator + - Best for: Production-grade testing, operator trials + - Complexity: Low (turnkey) + - Cost: €10,000 - €50,000/year + +**Recommendation:** Start with **Open5GS + UERANSIM** for Phase 1 development, upgrade to Amarisoft for operator validation in Phase 2. + +--- + +**Document Status:** Final +**Owner:** Nuvla Engineering / 5G-EMERGE Project +**Revision History:** +- v1.0 (2025-10-24): Initial compliance study diff --git a/docs/5g-emerge/ETSI-MEC/ETSI MEC _ Nuvla mapping.xlsx b/docs/5g-emerge/ETSI-MEC/ETSI MEC _ Nuvla mapping.xlsx new file mode 100644 index 000000000..4313d31de Binary files /dev/null and b/docs/5g-emerge/ETSI-MEC/ETSI MEC _ Nuvla mapping.xlsx differ diff --git a/docs/5g-emerge/ETSI-MEC/ETSI-Group-Presentation.md b/docs/5g-emerge/ETSI-MEC/ETSI-Group-Presentation.md new file mode 100644 index 000000000..330b50c2b --- /dev/null +++ b/docs/5g-emerge/ETSI-MEC/ETSI-Group-Presentation.md @@ -0,0 +1,1101 @@ +# Nuvla.io Platform: ETSI MEC Compliance Journey + +**Presentation for ETSI MEC Group** +**Date:** November 2025 +**Project:** 5G-EMERGE Initiative +**Presenter:** Nuvla.io Team + +--- + +## Agenda + +1. **Nuvla Platform Overview** + - What is Nuvla? + - Core Capabilities & Architecture + - Edge Computing Positioning + +2. **ETSI MEC Compliance Vision** + - Current State Assessment + - Core Standards Focus + - Compliance Roadmap + +3. **Implementation Strategy** + - MEC 003: Architectural Alignment + - MEC 010-2: Application Lifecycle Management + - MEC 037: Application Package Management + +4. **Technical Approach & Timeline** + +5. **Future Roadmap & Discussion** + +--- + +# Part 1: Nuvla Platform Overview + +--- + +## What is Nuvla? + +**Nuvla is a comprehensive edge-to-cloud management platform** that orchestrates applications across distributed computing infrastructure. + +### Mission +Enable organizations to: +- Deploy and manage edge applications at scale +- Orchestrate workloads across multi-cloud and edge environments +- Monitor and control distributed edge devices +- Implement secure, multi-tenant edge computing solutions + +### Origin +- Developed by SixSq (Switzerland) +- Built on 10+ years of cloud orchestration experience +- Production-proven with deployments across Europe +- Open architecture supporting multi-vendor ecosystems + +--- + +## Core Platform Capabilities + +### 1. **Edge Device Management (NuvlaBox)** + +- **Lifecycle Management:** Registration, activation, commissioning, decommissioning +- **Real-time Monitoring:** Telemetry collection, health status, resource utilization +- **Remote Operations:** SSH access, remote updates, reboot, diagnostics +- **Peripheral Management:** USB devices, sensors, actuators +- **Cluster Support:** Multi-node edge deployments + +**Key Metrics:** +- Supports 1000s of edge devices per installation +- Multi-architecture support (x86, ARM, RISC-V) +- Operating System agnostic (Linux, Windows, embedded) + +--- + +### 2. **Application Orchestration** + +- **Multi-Runtime Support:** Docker, Kubernetes, Helm charts, Docker Swarm +- **Deployment Models:** Single containers, microservices, distributed applications +- **Lifecycle Operations:** Deploy, start, stop, update, scale, clone, rollback +- **Version Management:** Application versioning, A/B deployments, canary releases +- **Infrastructure Abstraction:** Deploy once, run anywhere (cloud, edge, hybrid) + +**Supported Platforms:** +- Kubernetes (vanilla, AKS, GKE, EKS, k3s, MicroK8s) +- Docker Swarm +- Standalone Docker hosts + +--- + +### 3. **Multi-Tenancy & Security** + +- **Fine-grained Access Control:** Resource-level ACLs +- **Authentication:** OAuth2, OIDC, API keys, session-based auth +- **Authorization:** Role-based access control (RBAC) +- **Multi-tenancy:** Organizations, teams, users, resource isolation +- **Audit Trail:** Complete operation logging and compliance tracking + +**Security Features:** +- TLS/SSL everywhere +- Encrypted credentials storage +- VPN connectivity for edge devices +- Compliance with GDPR and industry standards + +--- + +### 4. **API-First Architecture** + +- **RESTful API:** CIMI-inspired (Cloud Infrastructure Management Interface) +- **Full CRUD Operations:** Create, Read, Update, Delete for all resources +- **Event-Driven:** Kafka-based event streaming +- **Async Job Processing:** ZooKeeper-backed job queue +- **Query & Filter:** Advanced search, pagination, aggregation + +**API Characteristics:** +- Comprehensive OpenAPI/Swagger documentation +- Client libraries (Python, JavaScript, CLI) +- Webhooks and event subscriptions +- Rate limiting and quota management + +--- + +### 5. **Data Management** + +- **Time-Series Data:** Elasticsearch-based telemetry storage +- **Metrics & Analytics:** Real-time monitoring, historical analysis +- **Data Streams:** NuvlaBox metrics, deployment logs, audit events +- **Retention Policies:** Configurable data lifecycle management + +**Data Capabilities:** +- Sub-second telemetry collection +- Multi-year data retention +- Aggregation and statistical analysis +- Export to external analytics platforms + +--- + +## Nuvla Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Nuvla API Server │ +│ (Edge Orchestration Core) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │ +│ │ Resource │ │ Event │ │ Job Queue │ │ +│ │ Management │ │ Streaming │ │ (Async Ops) │ │ +│ │ (CRUD API) │ │ (Kafka) │ │ (ZooKeeper) │ │ +│ └──────────────┘ └──────────────┘ └────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Multi-Tenancy & Security Layer │ │ +│ │ (ACL, RBAC, Authentication, Authorization) │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────┬───────────────────────────────────────┘ + │ + ┌────────────────┼────────────────┐ + │ │ │ + ┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐ + │ NuvlaBox │ │ NuvlaBox │ │ NuvlaBox │ + │ (Edge 1) │ │ (Edge 2) │ │ (Edge N) │ + └──────────┘ └──────────┘ └──────────┘ + │ │ │ + ┌────▼─────┐ ┌────▼─────┐ ┌────▼─────┐ + │Container │ │ K8s │ │ Docker │ + │ Apps │ │ Cluster │ │ Swarm │ + └──────────┘ └──────────┘ └──────────┘ +``` + +--- + +## Nuvla as MEC Orchestrator (MEO) + +### Current Positioning + +Nuvla already functions as a **MEC Orchestrator** providing: + +✅ **System-Level Orchestration** +- Multi-site application lifecycle management +- Cross-infrastructure deployment coordination +- Resource placement decisions + +✅ **Application Package Management** +- Docker and Kubernetes application packaging +- Module versioning and publishing +- Deployment template management + +✅ **Infrastructure Coordination** +- Edge device (MEC Host) management +- Multi-cloud connectivity +- Service mesh integration + +--- + +### MEC Architecture Alignment + +| **MEC Component** | **Nuvla Equivalent** | **Current Status** | +|-------------------|----------------------|--------------------| +| **MEC System** | Nuvla Platform | ✅ **Strong** (80%) | +| **MEC Host** | NuvlaBox | ✅ **Strong** (90%) | +| **MEC Orchestrator** | Deployment Manager | ⚠️ **Partial** (45%) | +| **MEC Platform** | API Server | ⚠️ **Partial** (40%) | +| **MEC Application** | Module/Deployment | ✅ **Good** (70%) | +| **MEC App Package** | Container Images | ⚠️ **Partial** (65%) | +| **MEC Services** | Infrastructure Services | 🔴 **Limited** (30%) | + +--- + +## Use Cases & Deployments + +### Current Production Use Cases + +1. **Smart Cities** + - Distributed sensor networks + - Real-time video analytics at the edge + - Traffic management systems + +2. **Industrial IoT** + - Factory automation + - Predictive maintenance + - Quality control with edge AI + +3. **Connected Vehicles** + - Fleet management + - Vehicle telemetry processing + - Edge-based route optimization + +4. **Retail & Hospitality** + - Point-of-sale systems + - Customer analytics + - Inventory management + +--- + +### Geographic Reach + +- **Europe:** Switzerland, France, Germany, UK, Spain +- **North America:** USA, Canada +- **Asia:** Pilot deployments in Singapore, Japan +- **Sectors:** Manufacturing, Transportation, Energy, Retail, Smart Cities + +--- + +# Part 2: ETSI MEC Compliance Vision + +--- + +## Why ETSI MEC Compliance? + +### Business Drivers + +1. **5G Network Evolution** + - Mobile operators deploying MEC infrastructure + - Need for standardized edge orchestration + - Integration with 5G Core and RAN + +2. **Multi-Vendor Ecosystems** + - Interoperability across MEC platforms + - Avoid vendor lock-in + - Standard APIs for application developers + +3. **Regulatory & Standards Compliance** + - ETSI standards increasingly referenced in tenders + - European regulatory frameworks (e.g., Gaia-X) + - Future-proofing platform investments + +4. **5G-EMERGE Project Requirements** + - EU-funded research requiring MEC compliance + - Focus on advanced edge computing scenarios + - Collaboration with telecom operators + +--- + +## Current Compliance Assessment + +### Overall Status: **~35% Baseline Compliance** + +**What This Means:** +- ✅ Strong edge infrastructure foundation +- ⚠️ MEC-specific APIs and abstractions missing +- 🔴 No mobile network integration (RNI, location services) +- ⚠️ Application package format needs standardization + +--- + +### Compliance by MEC Standard + +| **Standard** | **Title** | **Compliance** | **Priority** | +|--------------|-----------|----------------|--------------| +| **MEC 003** | Framework & Reference Architecture | 45% | Medium | +| **MEC 010-2** | Application Lifecycle Management | 40% | 🎯 **HIGH** | +| **MEC 037** | Application Package Descriptor | 30% | 🎯 **HIGH** | +| **MEC 040** | Federation Enablement | 25% | 🎯 **HIGH** | +| **MEC 021** | Application Mobility Service | 10% | 🎯 **HIGH** | +| **MEC 011** | Platform Application Enablement | 35% | Medium | +| **MEC 012** | Radio Network Information | 0% | Low* | +| **MEC 013** | Location API | 20% | Medium | +| **MEC 028** | WLAN Information | 15% | Medium | + +*Low priority due to lack of 3GPP access in typical deployments + +--- + +## Core Standards Focus (5G-EMERGE Phase 1) + +Our implementation focuses on **three foundational standards** that establish a solid MEC platform base: + +### 1. **ETSI GS MEC 003** - Framework & Reference Architecture +**Why Critical:** +- Defines MEC system components and interfaces +- Establishes architectural foundation for all other standards +- Required for proper component mapping and terminology + +**Current Gap:** Need formal mapping of Nuvla components to MEC architecture + +--- + +### 2. **ETSI GS MEC 010-2** - Application Lifecycle Management +**Why Critical:** +- Core to any MEC platform +- Enables standardized app deployment and operations +- Required for multi-vendor interoperability +- Foundation for application orchestration + +**Current Gap:** Missing MEC-specific lifecycle states, traffic/DNS rules + +--- + +### 3. **ETSI GS MEC 037** - Application Package Descriptor +**Why Critical:** +- Standard packaging format (TOSCA-based) +- App portability across MEC platforms +- Onboarding automation and validation +- Complements MEC 010-2 lifecycle management + +**Current Gap:** Using Docker/K8s formats instead of MEC TOSCA descriptors + +--- + +## Additional Standards (Future Roadmap) + +These standards represent natural evolution paths as the platform matures: + +### **ETSI GS MEC 040** - Federation Enablement +- Multi-operator edge deployments +- Cross-domain orchestration +- **Status:** Exploratory phase; requires multi-party partnerships + +### **ETSI GS MEC 021** - Application Mobility Service +- Application state migration between edge sites +- Mobile user experience optimization +- **Status:** Research phase; requires advanced infrastructure + +--- + +## Key Gaps Identified + +### 🔴 Critical Gaps + +1. **No MEC Service APIs** + - Missing Radio Network Information Service (RNIS) + - Missing Location Service API + - Missing Bandwidth Management Service + - Missing UE Identity Service + +2. **No Mobile Network Integration** + - No 3GPP interface support + - No Radio Access Network (RAN) awareness + - No subscriber/UE tracking + +3. **No MEC Application Enablement (Mp1)** + - Missing standardized app-to-platform interface + - No MEC service discovery + - No service dependency management + +--- + +### ⚠️ Major Gaps + +1. **MEC-Specific Lifecycle States** + - Current: Generic states (CREATED, STARTED, STOPPED) + - Needed: NOT_INSTANTIATED, INSTANTIATION_IN_PROGRESS, etc. + +2. **Traffic & DNS Rules Management** + - No traffic steering capabilities + - No DNS rule configuration + - No traffic classification + +3. **MEC Application Package Format** + - Current: Docker Compose, Helm charts + - Needed: ETSI MEC TOSCA-based descriptors + +4. **Federation Infrastructure** + - No inter-platform trust mechanisms + - No federated resource discovery + - No cross-operator orchestration + +--- + +### ✅ Existing Strengths (Build Upon) + +1. **Edge Device Management** + - NuvlaBox = strong MEC Host foundation + - Real-time telemetry and monitoring + - Remote management operations + +2. **Container Orchestration** + - Kubernetes and Docker support + - Multi-cloud deployment + - Application lifecycle management + +3. **Multi-Tenancy** + - Resource isolation + - Fine-grained access control + - Organization/team management + +4. **Geographic Awareness** + - Existing geo-location libraries + - Polygon/zone support + - Can be extended for MEC Location Service + +--- + +# Part 3: Implementation Strategy + +--- + +## Overall Approach + +### Design Principles + +1. **Adapter Pattern Over Rewrite** + - Create MEC facade layer over existing Nuvla resources + - Preserve backward compatibility + - Minimize disruption to existing deployments + +2. **Standards-First Development** + - Strict adherence to ETSI specifications + - API contract testing + - Conformance validation at each milestone + +3. **Incremental Delivery** + - Ship working features early + - Get feedback from pilots + - Iterate based on real-world usage + +4. **Leverage Existing Assets** + - Build on Nuvla's orchestration strengths + - Extend NuvlaBox capabilities + - Reuse security and multi-tenancy infrastructure + +--- + +## Implementation Roadmap (Phased Approach) + +``` +Phase 1: Core Foundation (10-12 months) → 70% Compliance + ├─ MEC 003: Architectural Alignment & Component Mapping + ├─ MEC 010-2: Application Lifecycle Management (complete) + ├─ MEC 037: TOSCA Package Support (complete) + └─ Essential Platform Services (registry, basic APIs) + +Phase 2: Platform Maturation (8-10 months) → 80% Compliance + ├─ MEC 011: Application Enablement APIs + ├─ Supporting Services (Location, WLAN info) + ├─ Production Hardening & Performance + └─ Conformance Testing & Validation + +Future Phases: Advanced Capabilities (Exploratory) + ├─ MEC 040: Federation (requires multi-operator partnerships) + ├─ MEC 021: Application Mobility (requires advanced infrastructure) + ├─ MEC 012: RNIS (requires 3GPP integration) + └─ Timeline: Subject to partnership opportunities and project evolution +``` + +--- + +## MEC 003: Architectural Alignment + +### Objective +Ensure Nuvla architecture maps correctly to MEC reference architecture. + +### Key Activities + +1. **Component Mapping Documentation** + - Map Nuvla resources to MEC entities + - Create terminology translation guide + - Update API documentation with MEC references + +2. **Interface Alignment** + - Identify Mm2, Mm3, Mm5, Mm9 interface requirements + - Design API endpoints for each interface + - Implement interface adapters + +3. **MEC Platform Service Registry** + - Extend infrastructure-service resource + - Add MEC service metadata + - Implement capability advertisement + - Create service discovery API + +**Timeline:** 4-6 weeks (parallel with early Phase 1) +**Effort:** 1 architect + 1 developer +**Deliverables:** Architecture alignment document, MEC terminology guide + +--- + +## MEC 010-2: Application Lifecycle Management + +### Objective +Implement ETSI-compliant application lifecycle operations. + +### Scope + +#### 1. **Application Lifecycle States** (4 weeks) +- Implement MEC state machine: + - NOT_INSTANTIATED + - INSTANTIATION_IN_PROGRESS + - INSTANTIATED + - TERMINATION_IN_PROGRESS + - TERMINATED + - OPERATION_IN_PROGRESS +- State transition validation +- Error handling and rollback + +--- + +#### 2. **Application Operations API** (6 weeks) +Implement MEC-compliant operations: + +- **Instantiate Application** + ``` + POST /mec/v2/app_instances + { + "appDId": "app-descriptor-id", + "appInstanceName": "my-edge-app", + "virtualResources": {...} + } + ``` + +- **Operate Application** (start, stop, restart) + ``` + POST /mec/v2/app_instances/{appInstanceId}/operate + { + "operationType": "START" + } + ``` + +- **Terminate Application** + ``` + POST /mec/v2/app_instances/{appInstanceId}/terminate + ``` + +--- + +#### 3. **Traffic Rules Management** (6 weeks) + +Implement traffic steering and classification: + +``` +POST /mec/v2/app_instances/{appInstanceId}/traffic_rules +{ + "trafficRuleId": "rule-001", + "filterType": "FLOW", + "priority": 1, + "trafficFilter": { + "srcAddress": ["192.168.1.0/24"], + "dstAddress": ["10.0.0.1"], + "srcPort": ["80", "443"] + }, + "action": "FORWARD_DECAPSULATED", + "dstInterface": "eth0" +} +``` + +**Components:** +- Traffic rule resource (CRUD) +- Integration with NuvlaBox networking +- Traffic classification engine +- Rule priority management + +--- + +#### 4. **DNS Rules Management** (4 weeks) + +DNS configuration for MEC applications: + +``` +POST /mec/v2/app_instances/{appInstanceId}/dns_rules +{ + "dnsRuleId": "dns-001", + "domainName": "myapp.mec.local", + "ipAddressType": "IP_V4", + "ipAddress": "10.0.1.100", + "ttl": 300 +} +``` + +**Components:** +- DNS rule resource +- Integration with DNS services +- Dynamic DNS updates +- Conflict resolution + +--- + +### MEC 010-2 Timeline Summary + +| Component | Duration | Dependencies | +|-----------|----------|--------------| +| State Machine | 4 weeks | None | +| Operations API | 6 weeks | State Machine | +| Traffic Rules | 6 weeks | Operations API | +| DNS Rules | 4 weeks | Operations API | +| Testing & Integration | 4 weeks | All above | +| **Total** | **14 weeks** | Sequential + parallel work | + +**Team:** 3-4 developers +**Deliverable:** Production-ready MEC 010-2 API (70% → 90% compliance) + +--- + +## MEC 037: Application Package Management + +### Objective +Support TOSCA-based MEC application descriptors and onboarding. + +### Scope + +#### 1. **TOSCA Parser Implementation** (8 weeks) + +Implement parser for MEC application descriptors: + +```yaml +tosca_definitions_version: tosca_simple_yaml_1_2 +description: MEC Application Descriptor + +metadata: + template_name: MyMECApp + template_version: 1.0 + +node_templates: + mec_app: + type: tosca.nodes.MEC.MECApplication + properties: + appDId: my-mec-app-001 + appName: My MEC Application + appProvider: SixSq + appSoftwareVersion: 1.0.0 + virtualComputeDescriptor: + virtualCpu: + numVirtualCpu: 2 + virtualMemory: + virtualMemSize: 4096 + requirements: + - location_service: + capability: tosca.capabilities.MEC.LocationService + - bandwidth: + capability: tosca.capabilities.MEC.BandwidthManagement +``` + +--- + +#### 2. **Package Validation** (4 weeks) + +- Schema validation against MEC 037 spec +- Security scanning +- Resource requirement validation +- Dependency checking + +#### 3. **Package Onboarding** (6 weeks) + +``` +POST /mec/v2/app_packages +Content-Type: multipart/form-data + +{ + "appPkgFile": , + "metadata": { + "appProvider": "SixSq", + "appVersion": "1.0.0" + } +} +``` + +**Components:** +- Package upload and storage +- Metadata extraction +- Image registry integration +- Version management + +--- + +#### 4. **Package to Deployment Translation** (6 weeks) + +Map TOSCA descriptors to Nuvla deployments: + +``` +TOSCA Package → Nuvla Module → Deployment Instance +``` + +**Challenges:** +- Translate TOSCA resource requirements to K8s/Docker +- Map MEC service requirements to Nuvla infrastructure services +- Handle TOSCA relationships and dependencies + +--- + +### MEC 037 Timeline Summary + +| Component | Duration | Dependencies | +|-----------|----------|--------------| +| TOSCA Parser (basic) | 6 weeks | None | +| Package Validation | 4 weeks | Parser | +| Onboarding API | 6 weeks | Validation | +| **Phase 1 Subtotal** | **12 weeks** | | +| Advanced TOSCA Features | 8 weeks | Phase 1 | +| Package Translation | 6 weeks | Phase 1 | +| Testing & Integration | 4 weeks | All | +| **Phase 2 Subtotal** | **12 weeks** | | +| **Total** | **24 weeks** | Across Phase 1 & 2 | + +**Team:** 2-3 developers +**Deliverable:** Full TOSCA support (30% → 90% compliance) + +--- + +--- + +## Advanced Standards: Future Exploration + +These standards represent important capabilities for advanced MEC scenarios. Their implementation will be considered based on market demand, partnership opportunities, and project evolution. + +### MEC 040: Federation Enablement + +**Vision:** Enable multi-operator MEC deployments with cross-domain orchestration. + +**Key Capabilities:** +- Federated trust and security models +- Cross-platform service discovery +- Federated resource management +- Multi-site application orchestration + +**Current Status:** Architectural research phase + +**Requirements for Implementation:** +- Partnerships with multiple MEC operators +- Legal frameworks for cross-operator data sharing +- Test federation infrastructure +- 12-18 months development timeline + +**Estimated Effort:** 40-50 weeks with specialized team + +--- + +### MEC 021: Application Mobility Service + +**Vision:** Seamless application migration between edge sites as users move. + +**Key Capabilities:** +- Application state capture and transfer +- Mobility trigger detection (location-based, load-based) +- Live migration with minimal downtime +- Context preservation across sites + +**Current Status:** Conceptual phase + +**Requirements for Implementation:** +- Advanced state management infrastructure +- Multi-site orchestration (builds on MEC 040) +- Network control for traffic switchover +- Mobile operator integration for UE tracking +- 9-12 months development timeline (after MEC 040) + +**Estimated Effort:** 30-40 weeks with specialized team + +**Note:** Best suited for specific use cases (connected vehicles, AR/VR, industrial robotics) + +--- + +### Strategic Considerations + +These advanced standards will be pursued when: + +1. **Core Foundation is Solid** + - MEC 003, 010-2, and 037 fully implemented and validated + - Production deployments demonstrating platform stability + +2. **Market Demand Exists** + - Clear customer requirements for federation or mobility + - Business cases justified for investment + +3. **Partnerships Established** + - Multi-operator agreements in place + - Access to required test infrastructure + - Co-development or co-funding opportunities + +4. **Technical Readiness** + - Team expertise developed through core implementation + - Infrastructure scaled for advanced scenarios + +--- + +# Part 4: Technical Approach & Timeline + +--- + +## Implementation Architecture + +### MEC Facade Layer + +Create a **MEC abstraction layer** that translates between MEC APIs and Nuvla resources: + +``` +┌──────────────────────────────────────────────────────┐ +│ MEC-Compliant APIs │ +│ (MEC 010-2, 037, 040, 021, 011, 013, ...) │ +└────────────────┬─────────────────────────────────────┘ + │ +┌────────────────▼─────────────────────────────────────┐ +│ MEC Adapter Layer │ +│ - State mapping (Nuvla ↔ MEC) │ +│ - Resource translation │ +│ - Event bridging │ +│ - API versioning │ +└────────────────┬─────────────────────────────────────┘ + │ +┌────────────────▼─────────────────────────────────────┐ +│ Nuvla Core Resources │ +│ (Module, Deployment, NuvlaBox, Infrastructure │ +│ Service, Data-Record, etc.) │ +└──────────────────────────────────────────────────────┘ +``` + +**Benefits:** +- ✅ Preserve backward compatibility +- ✅ Minimize core changes +- ✅ Independent API versioning +- ✅ Easy to extend with new MEC standards + +--- + + + +## Implementation Timeline + +**Target Completion: October 2026** + +**Core Standards Implementation:** +- MEC 003: Framework & Reference Architecture +- MEC 010-2: Application Lifecycle Management +- MEC 037: Application Package Descriptor + +**Expected Compliance Level:** 75-80% + +**Approach:** +- Phased development with iterative delivery +- Standards-first implementation +- Continuous validation and testing + +--- + +# Part 5: Summary & Discussion + +--- + +## Our Commitment + +**Core Standards Implementation:** +- Full implementation of MEC 003, 010-2, and 037 +- **Target Completion: October 2026** +- **Expected Compliance Level:** 75-80% + +**Target Use Cases:** +- Industrial IoT and smart manufacturing +- Smart cities and intelligent transportation +- Private 5G networks +- Enterprise edge computing + +**Future Roadmap:** +- MEC 040 (Federation) - based on partnership opportunities +- MEC 021 (Mobility) - based on market demand +- MEC 012 (RNIS) - requires 3GPP integration + +--- + +## Thank You + +### Contact Information + +**Nuvla.io Team** +Email: info@sixsq.com +Website: https://nuvla.io +GitHub: https://github.com/nuvla + +**5G-EMERGE Project** +Website: https://5g-emerge.eu + +--- + +### Next Steps + +1. **Implementation:** Execute Phase 1 core standards development +2. **Collaboration:** Open to pilot deployments and partnerships +3. **Standards Participation:** Engage with ETSI working groups + +--- + +### Appendix: Additional Resources + +**Available Documentation:** +- Complete ETSI MEC Gap Analysis (2,300+ lines) +- MEC 003 Architectural Mapping (in development) +- Implementation specifications (available on request) + +**Demo Environment:** +- Live Nuvla platform demo +- NuvlaBox edge device demonstrations +- Application deployment examples + +--- + +## Questions? + +--- + +*Thank you for your attention. We look forward to contributing to the ETSI MEC ecosystem.* +- TOSCA and application packaging experience +- Container orchestration (K8s, Docker) +- Network programming (traffic/DNS rules) + +--- + +#### **Phase 2: Maturation Team** (8-10 months) +- **2-3 Backend Developers** (maintain core team) +- **1-2 DevOps/Platform Engineers** +- **1 QA Engineer** (focus on conformance testing) +- **0.5 FTE Technical Lead** +- **0.5 FTE Security Specialist** (hardening phase) + +**Total:** ~5-7 FTEs + +**Focus:** +- Production readiness and performance +- Security and compliance +- Operator validation +- Documentation and training + +--- + +### Infrastructure Requirements + +**Development & Testing:** +- Multi-region cloud infrastructure (simulate edge sites) +- Kubernetes clusters (3-5 sites) +- Object storage (state snapshots) +- Networking lab (VPNs, traffic shaping) +- Mobile network test environment (Phase 4) + +**Estimated Infrastructure Cost:** €5K-€10K/month + +--- + +## Budget Estimate + +### By Phase + +| Phase | Duration | Team Size | Labor Cost* | Infrastructure | Total | +|-------|----------|-----------|-------------|----------------|-------| +| **Phase 1** | 10-12 mo | 6 FTEs | €500K-€600K | €60K | €560K-€660K | +| **Phase 2** | 8-10 mo | 5-7 FTEs | €350K-€500K | €50K | €400K-€550K | + +*Fully-loaded costs (salary, benefits, overhead, training) + +### Total Investment + +**Complete Core Implementation (Phases 1-2):** +- **Timeline:** 18-22 months +- **Budget:** €960K - €1.21M +- **Target Compliance:** 80% (core standards at 85-90%) + +### Return on Investment + +**What This Delivers:** +- Production-ready MEC platform +- Full compliance with MEC 003, 010-2, and 037 +- Foundation for future advanced capabilities +- Market-ready for MEC deployments +- Validated against conformance tests + +**Cost Comparison:** +- Building MEC platform from scratch: €3-5M, 3-4 years +- Commercial MEC platform licenses: €100K-€500K/year (recurring) +- Nuvla MEC enhancement: €1M, <2 years (leverages existing platform) + +--- + +## Success Metrics + +### Technical KPIs + +| Metric | Phase 1 | Phase 2 | Phase 3 | +|--------|---------|---------|---------| +| **MEC API Coverage** | 40% | 55% | 75% | +| **Standard Compliance** | 55% | 65% | 80% | +| **Response Time** | <200ms | <150ms | <100ms | +| **Uptime** | 99.5% | 99.7% | 99.9% | +| **Supported MEC Apps** | 10 | 50 | 500 | + +### Business KPIs + +- **Pilot Deployments:** 3+ by end of Phase 2 +- **Operator Partnerships:** 2+ by end of Phase 3 +- **Interoperability:** Compatible with 2+ MEC platforms +- **Developer Adoption:** <1 day to deploy first MEC app + +--- + +## Risk Management + +### Key Risks & Mitigation + +| Risk | Impact | Mitigation Strategy | +|------|--------|--------------------| +| **TOSCA Complexity** | Medium | Incremental parser implementation; leverage existing libraries | +| **Standards Evolution** | Medium | Active ETSI participation; extensible architecture design | +| **Integration Testing** | Medium | Early access to conformance test suites; continuous validation | +| **Team Ramp-up** | Low | Comprehensive MEC training; phased knowledge transfer | +| **Scope Creep** | Medium | Strict focus on core standards; defer advanced features | + +### Success Factors + +✅ **Clear Scope:** Focus on achievable core standards (MEC 003, 010-2, 037) +✅ **Proven Platform:** Build on established Nuvla infrastructure +✅ **Standards-First:** Strict adherence to ETSI specifications +✅ **Incremental Delivery:** Working features delivered throughout +✅ **Production Focus:** Enterprise-grade quality and performance + +--- + +# Part 5: Summary & Discussion + +--- + +## Our Commitment + +**Core Standards Implementation:** +- Full implementation of MEC 003, 010-2, and 037 +- Production-ready platform in 18-22 months +- 80% overall compliance with strong foundation + +**Target Use Cases:** +- Industrial IoT and smart manufacturing +- Smart cities and intelligent transportation +- Private 5G networks +- Enterprise edge computing + +**Future Roadmap:** +- MEC 040 (Federation) - based on partnership opportunities +- MEC 021 (Mobility) - based on market demand +- MEC 012 (RNIS) - requires 3GPP integration + +--- + +## Thank You + +### Contact Information + +**Nuvla.io Team** +Email: info@sixsq.com +Website: https://nuvla.io +GitHub: https://github.com/nuvla + +**5G-EMERGE Project** +Website: https://5g-emerge.eu + +--- + +### Next Steps + +1. **Implementation:** Execute Phase 1 core standards development +2. **Collaboration:** Open to pilot deployments and partnerships +3. **Standards Participation:** Engage with ETSI working groups + +--- + +### Appendix: Additional Resources + +**Available Documentation:** +- Complete ETSI MEC Gap Analysis (2,300+ lines) +- MEC 003 Architectural Mapping (in development) +- Implementation specifications (available on request) + +**Demo Environment:** +- Live Nuvla platform demo +- NuvlaBox edge device demonstrations +- Application deployment examples + +--- + +## Questions? + +--- + +*Thank you for your attention. We look forward to contributing to the ETSI MEC ecosystem.* diff --git a/docs/5g-emerge/ETSI-MEC/ETSI-MEC-009.md b/docs/5g-emerge/ETSI-MEC/ETSI-MEC-009.md new file mode 100644 index 000000000..08d71d8c9 --- /dev/null +++ b/docs/5g-emerge/ETSI-MEC/ETSI-MEC-009.md @@ -0,0 +1,3995 @@ +Multi-access Edge Computing (MEC); + +General principles, patterns and common aspects + +of MEC Service APIs + +``` +Disclaimer +``` +``` +The present document has been produced and approved by the Multi-access Edge Computing (MEC) ETSI Industry +Specification Group (ISG) and represents the views of those members who participated in this ISG. +It does not necessarily represent the views of the entire ETSI membership. +``` +GROUP SPECIFICATION + + +``` +Reference +RGS/MEC-0009v311ApiPrinciples +``` +``` +Keywords +API, MEC +``` +### ETSI + +``` +650 Route des Lucioles +F-06921 Sophia Antipolis Cedex - FRANCE +``` +``` +Tel.: +33 4 92 94 42 00 Fax: +33 4 93 65 47 16 +Siret N° 348 623 562 00017 - APE 7112B +Association à but non lucratif enregistrée à la +Sous-Préfecture de Grasse (06) N° w +``` +``` +Important notice +The present document can be downloaded from: +http://www.etsi.org/standards-search +The present document may be made available in electronic versions and/or in print. The content of any electronic and/or +print versions of the present document shall not be modified without the prior written authorization of ETSI. In case of any +existing or perceived difference in contents between such versions and/or in print, the prevailing version of an ETSI +deliverable is the one made publicly available in PDF format at http://www.etsi.org/deliver. +Users of the present document should be aware that the document may be subject to revision or change of status. +Information on the current status of this and other ETSI documents is available at +https://portal.etsi.org/TB/ETSIDeliverableStatus.aspx +If you find errors in the present document, please send your comment to one of the following services: +https://portal.etsi.org/People/CommiteeSupportStaff.aspx +``` +Notice of disclaimer & limitation of liability +The information provided in the present deliverable is directed solely to professionals who have the appropriate degree of +experience to understand and interpret its content in accordance with generally accepted engineering or +other professional standard and applicable regulations. +No recommendation as to products and services or vendors is made or should be implied. +No representation or warranty is made that this deliverable is technically accurate or sufficient or conforms to any law +and/or governmental rule and/or regulation and further, no representation or warranty is made of merchantability or fitness +for any particular purpose or against infringement of intellectual property rights. +In no event shall ETSI be held liable for loss of profits or any other incidental or consequential damages. + +Any software contained in this deliverable is provided "AS IS" with no warranties, express or implied, including but not +limited to, the warranties of merchantability, fitness for a particular purpose and non-infringement of intellectual property +rights and ETSI shall not be held liable in any event for any damages whatsoever (including, without limitation, damages +for loss of profits, business interruption, loss of information, or any other pecuniary loss) arising out of or related to the use +of or inability to use the software. + +``` +Copyright Notification +No part may be reproduced or utilized in any form or by any means, electronic or mechanical, including photocopying and +microfilm except as authorized by written permission of ETSI. +The content of the PDF version shall not be modified without the written authorization of ETSI. +The copyright and the foregoing restriction extend to reproduction in all media. +``` +``` +© ETSI 2021. +All rights reserved. +``` + +## Contents + + + +- Intellectual Property Rights +- Foreword +- Modal verbs terminology +- 1 Scope +- 2 References +- 2.1 Normative references +- 2.2 Informative references +- 3 Definition of terms, symbols and abbreviations +- 3.1 Terms +- 3.2 Symbols +- 3.3 Abbreviations +- 4 Design principles for developing RESTful MEC service APIs +- 4.1 REST implementation levels +- 4.2 General principles............................................................................................................................................. +- 4.3 Entry point of a RESTful MEC service API +- 4.4 API security and privacy considerations +- 5 Documenting RESTful MEC service APIs +- 5.1 RESTful MEC service API template +- 5.2 Conventions for names +- 5.2.1 Case conventions +- 5.2.2 Conventions for URI parts +- 5.2.2.1 Introduction +- 5.2.2.2 Path segment naming conventions +- 5.2.2.3 Query naming conventions +- 5.2.3 Conventions for names in data structures +- 5.3 Provision of an OpenAPI definition +- 5.4 Documentation of the API data model +- 5.4.1 Overview +- 5.4.2 Structured data types +- 5.4.3 Simple data types +- 5.4.4 Enumerations +- 5.4.5 Serialization +- 6 Patterns of RESTful MEC service APIs +- 6.1 Introduction +- 6.2 Void +- 6.3 Pattern: Resource identification +- 6.3.1 Description +- 6.3.2 Resource definition(s) and HTTP methods +- 6.4 Pattern: Resource representations and content format negotiation +- 6.4.1 Description +- 6.4.2 Resource definition(s) and HTTP methods +- 6.4.3 Resource representation(s) +- 6.4.4 HTTP headers +- 6.4.5 Response codes and error handling +- 6.5 Pattern: Creating a resource (POST) +- 6.5.1 Description +- 6.5.2 Resource definition(s) and HTTP methods +- 6.5.3 Resource representation(s) +- 6.5.4 HTTP headers +- 6.5.5 Response codes and error handling +- 6.5a Pattern: Creating a resource (PUT) +- 6.5a.1 Description +- 6.5a.2 Resource definition(s) and HTTP methods +- 6.5a.3 Resource representation(s) +- 6.5a.4 HTTP headers +- 6.5a.5 Response codes and error handling +- 6.6 Pattern: Reading a resource +- 6.6.1 Description +- 6.6.2 Resource definition(s) and HTTP methods +- 6.6.3 Resource representation(s) +- 6.6.4 HTTP headers +- 6.6.5 Response codes and error handling +- 6.7 Pattern: Queries on a resource +- 6.7.1 Description +- 6.7.2 Resource definition(s) and HTTP methods +- 6.7.3 Resource representation(s) +- 6.7.4 HTTP headers +- 6.7.5 Response codes and error handling +- 6.8 Pattern: Updating a resource (PUT) +- 6.8.1 Description +- 6.8.2 Resource definition(s) and HTTP methods +- 6.8.3 Resource representation(s) +- 6.8.4 HTTP headers +- 6.8.5 Response codes and error handling +- 6.9 Pattern: Updating a resource (PATCH) +- 6.9.1 Description +- 6.9.2 Resource definition(s) and HTTP methods +- 6.9.3 Resource representation(s) +- 6.9.4 HTTP headers +- 6.9.5 Response codes and error handling +- 6.10 Pattern: Deleting a resource.............................................................................................................................. +- 6.10.1 Description +- 6.10.2 Resource definition(s) and HTTP methods +- 6.10.3 Resource representation(s) +- 6.10.4 HTTP headers +- 6.10.5 Response codes and error handling +- 6.11 Pattern: Task resources +- 6.11.1 Description +- 6.11.2 Resource definition(s) and HTTP methods +- 6.11.3 Resource representation(s) +- 6.11.4 HTTP headers +- 6.11.5 Response codes and error handling +- 6.12 Pattern: REST-based subscribe/notify +- 6.12.1 Description +- 6.12.2 Resource definition(s) and HTTP methods +- 6.12.3 Resource representation(s) +- 6.12.4 HTTP headers +- 6.12.5 Response codes and error handling +- 6.12a Pattern: REST-based subscribe/notify with Websocket fallback +- 6.12a.1 Description +- 6.12a.2 Resource definition(s) and HTTP methods +- 6.12a.3 Resource representation(s) +- 6.12a.4 HTTP headers +- 6.12a.5 Response codes and error handling +- 6.13 Pattern: Asynchronous operations +- 6.13.1 Description +- 6.13.2 Resource definition(s) and HTTP methods +- 6.13.3 Resource representation(s) +- 6.13.4 HTTP headers +- 6.13.5 Response codes and error handling +- 6.14 Pattern: Links (HATEOAS) +- 6.14.1 Description +- 6.14.2 Resource definition(s) and HTTP methods +- 6.14.3 Resource representation(s) +- 6.14.4 HTTP headers +- 6.14.5 Response codes and error handling +- 6.15 Pattern: Error responses +- 6.15.1 Description +- 6.15.2 Resource definition(s) and HTTP methods +- 6.15.3 Resource representation(s) +- 6.15.4 HTTP headers +- 6.15.5 Response codes and error handling +- 6.16 Pattern: Authorization of access to a RESTful MEC service API using OAuth 2.0 +- 6.16.1 Description +- 6.16.2 Resource definition(s) and HTTP methods +- 6.16.3 Resource representation(s) +- 6.16.4 HTTP headers +- 6.16.5 Response codes and error handling +- 6.16.6 Discovery of the parameters needed for exchanges with the token endpoint +- 6.16.7 Scope values +- 6.17 Pattern: Representation of lists in JSON +- 6.17.1 Description +- 6.17.2 Representation as arrays +- 6.17.3 Representation as maps +- 6.18 Pattern: Attribute selectors +- 6.18.1 Description +- 6.18.2 Resource definition(s) and HTTP methods +- 6.18.3 Resource representation(s) +- 6.18.4 HTTP headers +- 6.18.5 Response codes and error handling +- 6.19 Pattern: Attribute-based filtering +- 6.19.1 Description +- 6.19.2 Resource definition(s) and HTTP methods +- 6.19.3 Resource representation(s) +- 6.19.4 HTTP headers +- 6.19.5 Response codes and error handling +- 6.20 Pattern: Handling of too large responses +- 6.20.1 Description +- 6.20.2 Resource definition(s) and HTTP methods +- 6.20.3 Resource representation(s) +- 6.20.4 HTTP headers +- 6.20.5 Response codes and error handling +- 7 Alternative transport mechanisms +- 7.1 Description +- 7.2 Relationship of topics, subscriptions and access rights +- 7.3 Serializers +- 7.4 Authorization of access to a service over alternative transports using TLS credentials +- Annex A (informative): REST methods................................................................................................ +- Annex B (normative): HTTP response status codes +- Annex C (informative): Richardson maturity model of REST APIs +- Annex D (informative): RESTful MEC service API template............................................................ +- Annex E (normative): Error reporting +- E.1 Introduction +- E.2 General mechanism +- E.3 Common error situations +- Annex F (informative): Change History +- History + + +## Intellectual Property Rights + +Essential patents + +IPRs essential or potentially essential to normative deliverables may have been declared to ETSI. The declarations +pertaining to these essential IPRs, if any, are publicly available for ETSI members and non-members, and can be +found in ETSI SR 000 314: "Intellectual Property Rights (IPRs); Essential, or potentially Essential, IPRs notified to +ETSI in respect of ETSI standards", which is available from the ETSI Secretariat. Latest updates are available on the +ETSI Web server (https://ipr.etsi.org/). + +Pursuant to the ETSI Directives including the ETSI IPR Policy, no investigation regarding the essentiality of IPRs, +including IPR searches, has been carried out by ETSI. No guarantee can be given as to the existence of other IPRs not +referenced in ETSI SR 000 314 (or the updates on the ETSI Web server) which are, or may be, or may become, +essential to the present document. + +Trademarks + +The present document may include trademarks and/or tradenames which are asserted and/or registered by their owners. +ETSI claims no ownership of these except for any which are indicated as being the property of ETSI, and conveys no +right to use or reproduce any trademark and/or tradename. Mention of those trademarks in the present document does +not constitute an endorsement by ETSI of products, services or organizations associated with those trademarks. + +DECT™, PLUGTESTS™, UMTS™ and the ETSI logo are trademarks of ETSI registered for the benefit of its +Members. 3GPP™^ and LTE™ are trademarks of ETSI registered for the benefit of its Members and of the 3GPP +Organizational Partners. oneM2M™ logo is a trademark of ETSI registered for the benefit of its Members and of the +oneM2M Partners. GSM® and the GSM logo are trademarks registered and owned by the GSM Association. + +## Foreword + +This Group Specification (GS) has been produced by ETSI Industry Specification Group (ISG) Multi-access Edge +Computing (MEC). + +## Modal verbs terminology + +In the present document "shall", "shall not", "should", "should not", "may", "need not", "will", "will not", "can" and +"cannot" are to be interpreted as described in clause 3.2 of the ETSI Drafting Rules (Verbal forms for the expression of +provisions). + +"must" and "must not" are NOT allowed in ETSI deliverables except when used in direct citation. + + +## 1 Scope + +The present document defines design principles for RESTful MEC service APIs, provides guidelines and templates for +the documentation of these, and defines patterns of how MEC service APIs use RESTful principles. + +## 2 References + +## 2.1 Normative references + +References are either specific (identified by date of publication and/or edition number or version number) or +non-specific. For specific references, only the cited version applies. For non-specific references, the latest version of the +referenced document (including any amendments) applies. + +Referenced documents which are not found to be publicly available in the expected location might be found at +https://docbox.etsi.org/Reference/. + +``` +NOTE: While any hyperlinks included in this clause were valid at the time of publication, ETSI cannot guarantee +their long term validity. +``` +The following referenced documents are necessary for the application of the present document. + +``` +[1] IETF RFC 7231: "Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc7231. +``` +``` +[2] IETF RFC 7232: "Hypertext Transfer Protocol (HTTP/1.1): Conditional Requests". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc7232. +``` +``` +[3] IETF RFC 5789: "PATCH Method for HTTP". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc5789. +``` +``` +[4] IETF RFC 6901: "JavaScript Object Notation (JSON) Pointer". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc6901. +``` +``` +[5] IETF RFC 7396: "JSON Merge Patch". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc7396. +``` +``` +[6] IETF RFC 6902: "JavaScript Object Notation (JSON) Patch". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc6902. +``` +``` +[7] IETF RFC 5261: "An Extensible Markup Language (XML) Patch Operations Framework Utilizing +XML Path Language (XPath) Selectors". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc5261. +``` +``` +[8] IETF RFC 6585: "Additional HTTP Status Codes". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc6585. +``` +``` +[9] IETF RFC 3986: "Uniform Resource Identifier (URI): Generic Syntax". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc63986. +``` +``` +[10] IETF RFC 8259: "The JavaScript Object Notation (JSON) Data Interchange Format". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc8259. +``` + +[11] W3C Recommendation 16 August 2006: "Extensible Markup Language (XML) 1.1" (Second +Edition). + +NOTE: Available at https://www.w3.org/TR/2006/REC-xml11-20060816/. + +[12] IETF RFC 8288: "Web Linking". + +NOTE: Available at https://tools.ietf.org/html/rfc8288. + +[13] Void. + +[14] IETF RFC 5246: "The Transport Layer Security (TLS) Protocol Version 1.2". + +NOTE: Available at https://tools.ietf.org/html/rfc5246. + +[15] IETF RFC 7807: "Problem Details for HTTP APIs". + +NOTE: Available at https://tools.ietf.org/html/rfc7807. + +[16] IETF RFC 6749: "The OAuth 2.0 Authorization Framework". + +NOTE: Available at https://tools.ietf.org/html/rfc6749. + +[17] IETF RFC 6750: "The OAuth 2.0 Authorization Framework: Bearer Token Usage". + +NOTE: Available at https://tools.ietf.org/html/rfc6750. + +[18] IETF RFC 7540: "Hypertext Transfer Protocol Version 2 (HTTP/2)". + +NOTE: Available at https://tools.ietf.org/html/rfc7540. + +[19] Void. + +[20] IETF RFC 3339: "Date and Time on the Internet: Timestamps". + +NOTE: Available at https://tools.ietf.org/html/rfc3339. + +[21] IETF RFC 4918: "HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)". + +NOTE: Available at https://tools.ietf.org/html/rfc4918. + +[22] IETF RFC 7233: " Hypertext Transfer Protocol (HTTP/1.1): Range Requests". + +NOTE: Available at https://tools.ietf.org/html/rfc7233. + +[23] IETF RFC 7235: "Hypertext Transfer Protocol (HTTP/1.1): Authentication". + +NOTE: Available at https://tools.ietf.org/html/rfc7235. + +[24] IETF RFC 8446: "The Transport Layer Security (TLS) Protocol Version 1.3". + +NOTE: Available at https://tools.ietf.org/html/rfc8446. + +[25] IETF RFC 6455: "The WebSocket Protocol". + +NOTE: Available at https://tools.ietf.org/html/rfc6455. + +[26] ETSI TS 129 122: "Universal Mobile Telecommunications System (UMTS); LTE; 5G; T +reference point for Northbound APIs" (3GPP TS 29.122). + +[27] ETSI TS 133 210: "Universal Mobile Telecommunications System (UMTS); LTE; 3G Security; +Network Domain Security (NDS); IP network layer security (3GPP TS 33.210)". + + +## 2.2 Informative references + +References are either specific (identified by date of publication and/or edition number or version number) or +non-specific. For specific references, only the cited version applies. For non-specific references, the latest version of the +referenced document (including any amendments) applies. + +``` +NOTE: While any hyperlinks included in this clause were valid at the time of publication, ETSI cannot guarantee +their long term validity. +``` +The following referenced documents are not necessary for the application of the present document but they assist the +user with regard to a particular subject area. + +``` +[i.1] ETSI GS MEC 001: "Multi-access Edge Computing (MEC); Terminology". +``` +``` +[i.2] William Durand: "Please. Don't Patch Like An Idiot". +``` +``` +NOTE: Available at http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/. +``` +``` +[i.3] Martin Fowler: "Richardson Maturity Model: steps toward the glory of REST". +``` +``` +NOTE: Available at http://martinfowler.com/articles/richardsonMaturityModel.html. +``` +``` +[i.4] JSON Schema, Draft Specification 2020-12, December 8, 2020. +``` +``` +NOTE: Referenced version available as Internet Draft (work in progress) at https://tools.ietf.org/html/draft- +bhutton-json-schema-00. All versions are available at http://json-schema.org/specification.html. +``` +``` +[i.5] W3C® Recommendation: "XML Schema Part 0: Primer Second Edition". +``` +``` +NOTE: Available at https://www.w3.org/TR/xmlschema-0/. +``` +``` +[i.6] ETSI GS MEC 011: "Multi-access Edge Computing (MEC); Edge Platform Application +Enablement". +``` +``` +[i.7] ETSI GS MEC 012: "Multi-access Edge Computing (MEC); Radio Network Information API". +``` +``` +[i.8] IANA: "Hypertext Transfer Protocol (HTTP) Status Code Registry". +``` +``` +NOTE: Available at http://www.iana.org/assignments/http-status-codes. +``` +``` +[i.9] MQTT Version 3.1.1 Plus Errata 01, OASIS™ Standard, 10 December 2015. +``` +``` +NOTE: Available at http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html. +``` +``` +[i.10] Apache Kafka®. +``` +``` +NOTE: Available at https://kafka.apache.org/. +``` +``` +[i.11] gRPC®. +``` +``` +NOTE: Available at http://www.grpc.io/. +``` +``` +[i.12] Protocol buffers. +``` +``` +NOTE: Available at https://developers.google.com/protocol-buffers/. +``` +``` +[i.13] IETF RFC 7519: "JSON Web Token (JWT)". +``` +``` +NOTE: Available at https://tools.ietf.org/html/rfc7519. +``` +``` +[i.14] OpenAPI™ Specification. +``` +``` +NOTE: Available at https://github.com/OAI/OpenAPI-Specification. +``` +``` +[i.15] ETSI TS 129 222 (V16.8.0): "Universal Mobile Telecommunications System (UMTS); LTE; 5G; +T8 reference point for Northbound APIs (3GPP TS 29.122 version 16.8.0 Release 16)". +``` + +## 3 Definition of terms, symbols and abbreviations + +## 3.1 Terms + +For the purposes of the present document, the terms given in ETSI GS MEC 001 [i.1] and the following apply: + +resource: object with a type, associated data, a set of methods that operate on it, and, if applicable, relationships to +other resources + +``` +NOTE: A resource is a fundamental concept in a RESTful API. Resources are acted upon by the RESTful API +using the Methods (e.g. POST, GET, PUT, DELETE, etc.). Operations on Resources affect the state of +the corresponding managed entities. +``` +## 3.2 Symbols + +Void. + +## 3.3 Abbreviations + +For the purposes of the present document, the abbreviations given in ETSI GS MEC 001 [i.1] and the following apply: + +``` +AA Authentication and Authorization +API Application Programming Interface +BYOT Bring Your Own Transport +CRUD Create, Read, Update, Delete +DDoS Distributed Denial of Service +DN Distinguished Name +GS Group Specification +HATEOAS Hypermedia As The Engine Of Application State +HTTP Hypertext Transfer Protocol +HTTPS HTTP Secure +IANA Internet Assigned Numbers Authority +IETF Internet Engineering Task Force +ISG Industry Specification Group +JSON JavaScript Object Notation +JWT JSON Web Token +MEC Multi-access Edge Computing +PKI Public Key Infrastructure +POX Plain Old XML +REST Representational State Transfer +RFC Request For Comments +RPC Remote Procedure Call +SCS/AS Services Capability Server/Application Server +SCEF Service Capability Exposure Function +TCP Transmission Control Protocol +TLS Transport Layer Security +UE User Equipment +URI Uniform Resource Indicator +XML eXtensible Markup Language +YAML YAML Ain't Markup Language +``` + +## 4 Design principles for developing RESTful MEC service APIs + +## 4.1 REST implementation levels + +The Richardson Maturity Model as defined in [i.3] breaks down the principal elements of a REST approach into three +steps. + +All RESTful MEC service APIs shall implement at least Level 2 of the Richardson Maturity Model explained in +annex C. + +It is recommended to implement Level 3 when applicable. + +## 4.2 General principles............................................................................................................................................. + +RESTful MEC service APIs are not technology implementation dependent. + +RESTful MEC service APIs embrace all aspects of HTTP/1.1 (IETF RFC 7231 [1]) including its request methods, +response codes, and HTTP headers. Support for PATCH (IETF RFC 5789 [3]) is optional. + +For each RESTful MEC service API specification, the following information should be included: + +- Purpose of the API. +- URIs of resources including version number. +- HTTP methods (IETF RFC 7231 [1]) supported. +- Representations supported: JSON and, if applicable, XML. +- Response schema(s). +- Request schema(s) when PUT, POST, or PATCH are supported. +- Links supported (Optional in Level 2 APIs). +- Response status codes supported. + +Since the release of HTTP/1.1, major revisions have been introduced through HTTP/2 (IETF RFC 7540 [18]) including +binary serialization in place of textual, single TCP connection, full multiplexing, header compression and server push. +MEC system deployments may utilize HTTP/2. However, this is transparent to the RESTful MEC service APIs, as the +main semantic of HTTP has been retained in HTTP/2 thereby providing backwards compatibility. If HTTP/2 (IETF +RFC 7540 [18]) is supported, its use shall be negotiated as specified in section 3 of IETF RFC 7540 [18]. + +## 4.3 Entry point of a RESTful MEC service API + +Entry point for a RESTful MEC service API: + +- Needs to have one and exactly one entry point. The URI of the entry point needs to be communicated to API + clients so that they can find the API. +- It is common for the entry point to contain some or all of the following information: + - Information on API version, supported features, etc. + - A list of top-level collections. + - A list of singleton resources. + + +- Any other information that the API designer deemed useful, for example a small summary of operating + status, statistics, etc. +- It can be made known via different means: +- Client discovers automatically the entry point and meaning of the API. +- Client developer knows about the API at time of client development. + +## 4.4 API security and privacy considerations + +To allow proactive protection of the APIs against the known security and privacy issues, e.g. DDoS, frequency attack, +unintended or accidental information disclosure, etc. the design for a secure API should consider at least the following +aspects: + +- Ability to control the frequency of the API calls (calls/min), e.g. by supporting the definition of a validity time + or expiration time for a service response. +- Anonymization of the real identities. +- Authorization of the applications based on the sensitivity of the information exposed through the API. + +## 5 Documenting RESTful MEC service APIs + +## 5.1 RESTful MEC service API template + +Annex D defines a template for the documentation of RESTful MEC service APIs. Examples how to use this template +can for instance be found in ETSI GS MEC 011 [i.6] and ETSI GS MEC 012 [i.7]. + +## 5.2 Conventions for names + +## 5.2.1 Case conventions + +The following case conventions for names and strings are used in the RESTful MEC service APIs: + +``` +1) UPPER_WITH_UNDERSCORE +``` +``` +All letters of a string are capital letters. Digits are allowed but not at the first position. Word boundaries are +represented by the underscore "_" character. No other characters are allowed. +``` +``` +EXAMPLE 1: +``` +``` +a) ETSI_MEC_MANAGEMENT; +``` +``` +b) MULTI_ACCESS_EDGE_COMPUTING. +``` +``` +2) lower_with_underscore +``` +``` +All letters of a string are lowercase letters. Digits are allowed but not at the first position. Word boundaries are +represented by the underscore "_" character. No other characters are allowed. +``` +``` +EXAMPLE 2: +``` +``` +a) etsi_mec_management; +``` +``` +b) multi_access_edge_computing. +``` + +``` +3) UpperCamel +``` +``` +A string is formed by concatenating words. Each word starts with an uppercase letter (this implies that the +string starts with an uppercase letter). All other letters are lowercase letters. Digits are allowed but not at the +first position. No other characters are allowed. Abbreviations follow the same scheme (i.e. first letter +uppercase, all other letters lowercase). +``` +``` +EXAMPLE 3: +``` +``` +a) EtsiMecManagement; +``` +``` +b) MultiAccessEdgeComputing. +``` +``` +4) lowerCamel +``` +``` +As UpperCamel, but with the following change: The first letter of a string shall be lowercase (i.e. the first +word starts with a lowercase letter). +``` +``` +EXAMPLE 4: +``` +``` +a) etsiMecManagement; +``` +``` +b) multiAccessEdgeComputing. +``` +## 5.2.2 Conventions for URI parts + +## 5.2.2.1 Introduction + +Based on IETF RFC 3986 [9], the parts of the URI syntax that are relevant in the context of the RESTful MEC service +APIs are as follows: + +- Path, consisting of segments, separated by "/" (e.g. segment1/segment2/segment3). +- Query, consisting of pairs of parameter name and value (e.g. ?org=etsi&isg=mec, where two pairs are + presented). + +## 5.2.2.2 Path segment naming conventions + +``` +a) Each path segment of a resource URI which represents a constant string shall use lower_with_underscore. +Only letters, digits and underscore "_" are allowed. +``` +``` +EXAMPLE 1: etsi_mec_management +``` +``` +b) If a resource represents a collection of entities, and the last path segment of that resource's URI is a string +constant, the last path segment shall be plural. +``` +``` +EXAMPLE 2: .../prefix/api/v1/users +``` +``` +c) If a resource is not a task resource and the last path segment of that resource's URI is a string constant, the last +path segment should be a (composite) noun. +``` +``` +EXAMPLE 3: .../prefix/api/v1/users +``` +``` +d) For resources that are task resources, the last path segment of the resource URI should be a verb, or at least +start with a verb. +``` +``` +EXAMPLE 4: +``` +``` +.../app_instances/{appInstanceId}/instantiate +``` +``` +.../app_instances/{appInstanceId}/do_something_else +``` +``` +e) A name that represents a URI path segment or multiple URI path segments in the API documentation but +serves as a placeholder for an actual value created at runtime (URI path variable) shall use lowerCamel, and +shall be surrounded by curly brackets. +``` + +``` +EXAMPLE 5: {appInstanceId} +``` +``` +f) Once a variable is replaced at runtime by an actual string, the string shall follow the rules for a path segment or +sequence of path segments (depending on whether the variable represents a single path segment or a sequence +thereof) defined in IETF RFC 3986 [9]. IETF RFC 3986 [9] disallows certain characters from use in a path +segment. Each actual RESTful MEC service API specification shall define this restriction to be followed when +generating values for path segment variables, or propose a suitable encoding (such as percent-encoding +according to IETF RFC 3986 [9]), to escape such characters if they can appear in input strings intended to be +substituted for a path segment variable. +``` +## 5.2.2.3 Query naming conventions + +``` +a) Parameter names in queries shall use lower_with_underscore. +``` +``` +EXAMPLE 1: ?isg_name=MEC +``` +``` +b) Variables that represent actual parameter values in queries shall use lowerCamel and shall be surrounded by +curly brackets. +``` +``` +EXAMPLE 2: ?isg_name={chooseAName} +``` +``` +c) Once a variable is replaced at runtime by an actual string, the convention defined in clause 5.2.2.2 item f) +applies to that string. +``` +## 5.2.3 Conventions for names in data structures + +The following syntax conventions apply when defining the names for attributes and parameters in the RESTful MEC +service API data structures: + +``` +a) Names of attributes/parameters shall be represented using lowerCamel. +``` +``` +EXAMPLE 1: appName. +``` +``` +b) Names of arrays and maps (i.e. those with cardinality 1..N or 0..N) shall be plural rather than singular. +``` +``` +EXAMPLE 2: users, mecApps. +``` +``` +c) The identifier of a data structure via which this data structure can be referenced externally should be named +"id". +``` +``` +d) Each value of an enumeration types shall be represented using UPPER_WITH_UNDERSCORE. +``` +``` +EXAMPLE 3: NOT_INSTATIATED. +``` +``` +e) The names of data types shall be represented using UpperCamel. +``` +``` +EXAMPLE 4: ResourceHandle, AppInstance. +``` +## 5.3 Provision of an OpenAPI definition + +An ETSI ISG MEC GS defining a RESTful MEC service API should provide a supplementary description file (or +supplementary description files) compliant to the OpenAPI specification [i.14], which inherently include(s) a definition +of the data structures of the API in JSON schema or YAML format. A description file is machine readable facilitating +content validation and autocreation of stubs for both the service client and server. A link to the specific repository +containing the file(s) shall be provided. All API repositories can be accessed from https://forge.etsi.org. The file (or +files) shall be informative. In case of a discrepancy between supplementary description file(s) and the underlying +specification, the underlying specification shall take precedence. + + +## 5.4 Documentation of the API data model + +## 5.4.1 Overview + +Clause 5.4 and its clauses specify provisions for API data model documentation for ETSI ISG MEC GSs defining +RESTful MEC service APIs. Clause 5 in annex D provides a related data model template. + +The data model shall be defined using a tabular format as described in the following clauses. The name of the data type +shall be documented appropriately in the heading of the clause and in the caption of the table, preferably as defined in +clause 5.2.2 and in annex D. + +## 5.4.2 Structured data types + +Structured data types shall be documented in tabular format, as in table 5.4.2-1 (one table per named data type). + +``` +Table 5.4.2-1: Template for a table defining a named structured data type +``` +``` +Attribute name Data type Cardinality Description +``` +The following provisions apply to the content of the table: + +``` +1) "Attribute name" shall provide the name of the attribute in lowerCamel. +``` +``` +2) "Data type" shall provide one of the following: +``` +``` +a) The name of a named data type (structured, simple or enum) that is defined elsewhere in the document +where the data type is specified, or in a referenced document. In case of a referenced type from another +document, a reference to the defining document should be included in the "Description" column unless +included in a global clause. +``` +``` +b) An indication of the definition of an inlined nested structure. In case of inlining a structure, the "Data +type" column shall contain the string "Structure (inlined)", and all attributes of the inlined structure shall +be prefixed with one or more closing angular brackets ">", where the number of brackets represents the +level of nesting. +``` +``` +c) An indication of the definition of an inlined enumeration type. In case of inlining an enumeration type, +the "Data type" column shall contain the string "Enum (inlined)", and the "Description" column shall +contain the allowed values and (optionally) their meanings. There are two possible ways to define enums +(see clause 5.4.4): just to define the valid enum values or to define the valid values and their mapping to +integers. It is good practice not to mix the two patterns in the same data structure. +``` +``` +3) If the maximum cardinality is greater than one, "Data type" may indicate the format of the list of values. If it is +an array, the format of that list should be indicated by using the key word "array()". If it is a map, the +format shall be indicated by using the key word "map()". In both cases, indicates the data type +of the individual list entries. In case neither "map" nor "array" is given and the maximum cardinality is greater +than one, "array" is assumed as default. The presence or absence of the indication of "array" should be +consistent between all data types in the scope of an API. +``` +``` +4) "Cardinality" defines the allowed number of occurrences, either as a single value, or as two values indicating +lower bound and upper bound, separated by "..". A value shall be either a non-negative integer number or an +uppercase letter that serves as a placeholder for a variable number (e.g. N). +``` +``` +5) "Description" describes the meaning and use of the attribute and may contain normative statements. In case of +an inlined enumeration type, the "Description" column shall define the allowed values and (optionally) their +meanings, as follows: "Permitted values:" on one line, followed by one paragraph of the following format for +each value: "- : ". +``` +Table 5.4.2-2 provides an example. + + +``` +Table 5.4.2-2: Example of a structured data type definition +``` +Attribute name Data type Cardinality Description +type FooBarType 1 Indicates whether this is a foo, boo or hoo stream. +entryIdx array(UnsignedInt) 0..N The index of the entry in the signalling table for correlation +purposes, starting at 0. +fooBarType Enum (inlined) +1 Signals the type of the foo bar. + +Permitted values: +BIG_FOOBAR: Signals a big foobar. +SMALL_FOOBAR: Signals a small foobar. +fooBarColor Enum (inlined) +1 Signals the color of the foo bar. + +Permitted values: +1 = RED_FOOBAR: Signals a red foobar. +2 = GREEN_FOOBAR: Signals a green foobar. +firstChoice MyChoiceOneType 0..1 First choice. See note. +secondChoice map(MyChoiceTwoType) 0..N Second choice. See note. +nestedStruct Structure (inlined) 0..1 A structure that is inlined, rather than referenced via an +external type. +> someId String 1 An identifier. The level of nesting is indicated by ">". +> myNestedStruct array(Structure(inlined)) 0..N Another nested structure, one level deeper. +>> child String 1 Child node at nesting level 2, indicated by ">>" +NOTE: One of "firstChoice" or at least one of "secondChoice" but not both shall be present. + +## 5.4.3 Simple data types + +``` +Simple data types shall be documented in tabular format, as in table 5.4.3-1 (one table row per simple data type). +``` +``` +Table 5.4.3-1: Simple data types +``` +``` +Type name Description +``` +``` +The following provisions shall be applied to the content of the table: +``` +``` +1) "Type name" provides the name of the simple data type. +``` +``` +2) "Description" describes the meaning and use of the data type and may contain normative statements. +``` +``` +Table 5.4.3-2 provides an example. +``` +``` +Table 5.4.3-2: Example of a simple data type definition +``` +``` +Type name Description +DozenInt An integral number with a minimum value of 1 and a maximum value of 12. +``` +## 5.4.4 Enumerations + +``` +Enumerations can be specified just as a set of values, or as a set of values mapped to integers. +``` +``` +Enumeration types shall be documented in tabular format, as in table 5.4.4-1 or table 5.4.4-2 (one table row per +enumeration value, one table per enumeration type). +``` +``` +The following provisions shall be applied to the content of the table: +``` +``` +1) "Enumeration value" provides a mnemonic identifier of the enum value, optionally with an integer to which +the value is mapped. +``` +``` +2) "Description" describes the meaning of the value and may contain normative statements. +``` + +``` +If the intent is to map the enum values to strings (often used in JSON), or only to define the valid values (with the intent +to define their mappings to integers in a different place, specific to certain serializers), the format defined in +table 5.4.4-1 shall be used. +``` +``` +Table 5.4.4-1: Enumeration type +``` +``` +Enumeration value Description +A_VALUE The description of this enum value +ANOTHER_VALUE The description of this other enum value +``` +``` +If the intent is to map the enum values to integers independent of the serializer, the format in defined in table 5.4.4- +shall be used. "" in the table below shall be replaced by an actual integer value. +``` +``` +Table 5.4.4-2: Enumeration type with mapping to integer values +``` +``` +Enumeration value Description + = A_VALUE The description of this enum value + = ANOTHER_VALUE The description of this other enum value +``` +## 5.4.5 Serialization + +``` +This clause only applies to the serialization of the top-level named data types that define the structure of a resource +representation in the payload body of an HTTP request or response, but not of named data types that are referenced +from other named data types. In the template in annex D, such data types represent resources ("resource data types"), +subscriptions ("subscription data types") or notifications ("notification data types"). +``` +``` +When the data model is serialized as XML, the name of the applicable named top-level data type shall be converted to +lowerCamel (see clause 5.2.1) and used as the name of the root element. +``` +``` +When the data model is serialized as JSON, no root element shall be synthesized, but all attributes of the applicable +resource, subscription or notification data type shall appear at the root level (under consideration of their cardinality). +Individual APIs may deviate from this rule, for example when re-using pre-existing data models. Such deviation shall +be documented in the API specification. +``` +``` +The following examples illustrate this convention. Assume the resource data type "Person" is defined as in table 5.4.5-1. +The XML serialization is illustrated in example 1 and the JSON serialization is illustrated in example 2. +``` +``` +Table 5.4.5-1: Example: Definition of the "PersonData" data type +``` +Attribute name Data type Cardinality Description +lastName String 1 The surname of the person +firstName String 1 The first name of the person. +address Structure (inlined) 0..1 The address of the person, if known. +>street String 1 The street +>number Integer 1 The number of the house or apartment +>city String 1 The city + +``` +EXAMPLE 1: XML serialization: + +Doe +John +
+Route des Lucioles +650 +Sophia Antipolis +
+
+``` + +``` +EXAMPLE 2: JSON serialization: +``` +{ +"lastName": "Doe", +"firstName": "John", +"address": { +"street": "Route des Lucioles", +"number": 650, +"city": "Sophia Antipolis" +} +} + +## 6 Patterns of RESTful MEC service APIs + +## 6.1 Introduction + +This clause describes patterns to be used to model common operations and data types in the RESTful MEC service +APIs. The defined patterns are used consistently throughout different RESTful MEC service APIs as defined by ETSI +ISG MEC. + +For RESTful APIs exposed by MEC services designed by third parties, it is recommended to use these patterns if and +where applicable. + +## 6.2 Void + +## 6.3 Pattern: Resource identification + +## 6.3.1 Description + +Every resource is identified by at least one resource URI. A resource URI identifies at most one resource. + +## 6.3.2 Resource definition(s) and HTTP methods + +The syntax of each resource URI shall follow IETF RFC 3986 [9]. In the RESTful MEC service APIs, the resource URI +structure shall be as follows: + +``` +{apiRoot}/{apiName}/{apiVersion}/{apiSpecificSuffixes} +``` +"apiRoot" consists of the scheme ("https"), host and optional port, and an optional prefix string. "apiName" defines the +name of the API. The "apiVersion" represents the version of the API. "apiSpecificSuffixes" define the tree of resource +URIs in a particular API. The combination of "apiRoot", "apiName" and "apiVersion" is called the root URI. "apiRoot" +is under control of the deployment, whereas the remaining parts of the URI are under control of the API specification. + +All RESTful MEC service APIs shall support HTTP over TLS (also known as HTTPS) using TLS version 1.2 as +defined by IETF RFC 5246 [14]). TLS 1.3 (including the new specific requirements for TLS 1.2 implementations) +defined by IETF RFC 8446 [24] should be supported. HTTP without TLS shall not be used. Versions of TLS earlier +than 1.2 shall neither be supported nor used. If HTTP/2 (IETF RFC 7540 [18]) is supported, its use shall be negotiated +as specified in section 3 of IETF RFC 7540 [18]. + +TLS implementations should meet or exceed the security algorithm, key length and strength requirements specified in +clause 6.2.3 (if TLS version 1.2 as defined by IETF RFC 5246 [14] is used) or clause 6.2.2 (if TLS version 1.3 as +defined by IETF RFC 8446 [24] is used) of ETSI TS 133 210 [27] (3GPP Release 16 or later). + +``` +NOTE: This means that for HTTP/2 over TLS connections, negotiation uses TLS with the application-layer +protocol negotiation (ALPN) extension, whereas for unencrypted HTTP/2 connections, negotiation is +based on the HTTP Upgrade mechanism, or HTTP/2 is used immediately based on prior knowledge. +``` +With every HTTP method, exactly one resource URI is passed in the request to address one particular resource. + + +## 6.4 Pattern: Resource representations and content format negotiation + +## 6.4.1 Description + +Resource representations are an important concept in REST. Actually, a resource representation is a serialization of the +resource state in a particular content format. A resource representation is included in the payload body of an HTTP +request or response. It depends on the HTTP method whether a representation is required or not allowed in a request, as +defined in IETF RFC 7231 [1] (see table 6.4.1-1). If no representation is provided in a response, this shall be signalled +by the "204 No Content" response code. + +``` +Table 6.4.1-1: Payload bodies requirements in HTTP requests for the different HTTP methods +``` +``` +HTTP method Payload body is... +GET unspecified; not recommended +PUT required +POST required +PATCH required +DELETE unspecified; not recommended +``` +HTTP (IETF RFC 7231 [1]) provides a mechanism to negotiate the content format of a representation. Each ETSI MEC +API specification defines the content formats that are mandatory or optional by the server to support for that API; the +client may use any of these. Examples of content types are JSON (IETF RFC 8259 [10]) and XML [11]. In HTTP +requests and responses, the "Content-Type" HTTP header is used to signal the format of the actual representation +included in the payload body. If the format of the representation in an HTTP request is not supported by the server, it +responds with a "415 Unsupported Media Type" response code. The content formats that a client supports in a HTTP +response are signalled by the "Accept" HTTP header of the HTTP request. If the server cannot provide any of the +accepted formats, it returns the "406 Not Acceptable" response code. + +## 6.4.2 Resource definition(s) and HTTP methods + +This pattern is applicable to any resource and any HTTP method. + +## 6.4.3 Resource representation(s) + +This pattern is applicable to any resource representation. + +## 6.4.4 HTTP headers + +The client uses the "Accept" HTTP header to signal to the server the content formats it supports. It is also possible to +provide priorities. The HTTP specification can be found in IETF RFC 7231 [1]. + +As defined in the HTTP specification, both client and server use the "Content-Type" HTTP header to signal the content +format of the payload included in the payload body of the request or response, if a payload body is present. + +For the RESTful MEC service APIs, the following applies: In the "Accept" and "Content-Type" HTTP headers, the +string "application/json" shall be used to signal the use of the JSON format (IETF RFC 8259 [10]) and +"application/xml" shall be used to signal the use of the XML format [11]. + +## 6.4.5 Response codes and error handling + +Servers that do not support the content format of the representation received in the payload body of a request return the +"415 Unsupported Media Type" response code. + +A server returns "406 Not Acceptable" in a HTTP response if it cannot provide any of the formats signalled by the +client in the "Accept" HTTP header of the associated HTTP request. + +A server that wishes to omit the payload body in a successful response returns "204 No Content" instead of "200 OK". +This can make sense for DELETE, PUT and PATCH, but makes no sense for GET, and makes rarely sense for POST. + + +## 6.5 Pattern: Creating a resource (POST) + +## 6.5.1 Description + +This clause describes the "resource creation by POST" mode, where the client requests the origin server to create a new +resource under a parent resource, i.e. the URI that identifies the created resource is under control of the server. This +pattern shall be used for resource creation if the resource identifiers under the parent resource are managed by the server +(see clause 6.5a for an alternative). + +In order to request resource creation, the client sends a POST request specifying the resource URI of the parent resource +and includes a representation of the resource to be created. The server generates a name for the new resource that is +unique for all child resources in the scope of the parent resource, and concatenates this name with the resource URI of +the parent resource to form the resource URI of the child resource. The server creates the new resource, and returns in a +"201 Created" response a representation of the created resource along with a "Location" HTTP header field that +contains the resource URI of this resource. + +Figure 6.5.1-1 illustrates creating a resource by POST. + +``` +Figure 6.5.1-1: Resource creation flow (POST) +``` +## 6.5.2 Resource definition(s) and HTTP methods + +The following resources are involved: + +``` +1) parent resource: A container resource that can hold zero or more child resources; +``` +``` +2) created resource: A child resource of a container resource that is created as part of the operation. The resource +URI of the child resource is a concatenation of the resource URI of the parent resource with a string that is +chosen by the server, and that is unique in the scope of the parent resource URI. +``` +The HTTP method shall be POST. + +## 6.5.3 Resource representation(s) + +The payload body of the request shall contain a representation of the resource to be created. The payload body of the +response shall contain a representation of the created resource. + +``` +NOTE: Compared to the payload body passed in the request (ResourceRepresentation in figure 6.5.1-1), the +payload body in the response (ResourceRepresentation' in figure 6.5.1-1) may be different, as the +resource creation process may have modified the information that has been passed as input. +``` +## 6.5.4 HTTP headers + +Upon successful resource creation, the "Location" HTTP header field in the response shall be populated with the URI of +the newly created resource. + + +## 6.5.5 Response codes and error handling + +Upon successful resource creation, "201 Created" shall be returned. On failure, the appropriate error code (see annex B) +shall be returned. + +Resource creation can also be asynchronous in which case "202 Accepted" shall be returned instead of "201 Created". +See clause 6.13 for more details about asynchronous operations. + +## 6.5a Pattern: Creating a resource (PUT) + +## 6.5a.1 Description + +This clause describes the "resource creation by PUT" mode, where the client requests the origin server to create a new +resource, i.e. the URI that identifies the created resource is under control of the client. + +``` +NOTE: The parent resource in this mode is implicit, i.e. it can be derived from the resource URI of the created +resource, but is not provided explicitly in the request. +``` +This pattern shall be used for resource creation if the resource identifiers under the parent resource are managed by the +client (see clause 6.5a for an alternative). + +In order to request resource creation, the client sends a PUT request specifying the resource URI of the resource to be +created, and includes a representation of the resource to be created. The server creates the new resource, and returns in a +"201 Created" response a representation of the created resource along with a "Location" HTTP header field that +contains the resource URI of this resource. + +Figure 6.5a.1-1 illustrates creating a resource by PUT. + +``` +Figure 6.5a.1-1: Resource creation flow (PUT) +``` +## 6.5a.2 Resource definition(s) and HTTP methods + +The following resource is involved: + +``` +1) created resource: A resource that is created as part of the operation. The resource URI of that resource is +passed by the client in the PUT request. +``` +The HTTP method shall be PUT. + + +## 6.5a.3 Resource representation(s) + +The payload body of the request shall contain a representation of the resource to be created. The payload body of the +response shall contain a representation of the created resource. + +``` +NOTE: Compared to the payload body passed in the request (ResourceRepresentation in figure 6.5a.1-1), the +payload body in the response (ResourceRepresentation' in figure 6.5a.1-1) may be different, as the +resource creation process may have modified the information that has been passed as input. +``` +## 6.5a.4 HTTP headers + +Upon successful resource creation, the "Location" HTTP header field in the response shall be populated with the URI of +the newly created resource. + +## 6.5a.5 Response codes and error handling + +The server shall check whether the resource URI of the resource to be created does not conflict with the resource URIs +of existing resources (i.e. whether or not the resource requested to be created already exists). + +In case the resource does not yet exist: + +- Upon successful resource creation, "201 Created" shall be returned. Upon failure, the appropriate error code + (see annex B) shall be returned. +- Resource creation can also be asynchronous in which case "202 Accepted" shall be returned instead of "201 + Created". See clause 6.13 for more details about asynchronous operations. + +In case the resource already exists: + +- If the "Update by PUT" operation is not supported for the resource, the request shall be rejected with "403 + Forbidden", and a "ProblemDetails" payload should be included to provide more information about the error. +- If the "Update by PUT" operation is supported for the resource, interpret the request as an update request, + i.e. the request shall be processed as defined in clause 6.8. + +## 6.6 Pattern: Reading a resource + +## 6.6.1 Description + +This pattern obtains a representation of the resource, i.e. reads a resource, by using the HTTP GET method. For most +resources, the GET method should be supported. An exception is task resources (see clause 6.11); these cannot be read. + +Figure 6.6.1-1 illustrates reading a resource. + +``` +Figure 6.6.1-1: Reading a resource +``` +## 6.6.2 Resource definition(s) and HTTP methods + +This pattern is applicable to any resource that can be read. The HTTP method shall be GET. + + +## 6.6.3 Resource representation(s) + +The payload body of the request shall be empty; the payload body of the response shall contain a representation of the +resource that was read, if successful. + +## 6.6.4 HTTP headers + +No specific provisions for HTTP headers for this pattern. + +## 6.6.5 Response codes and error handling + +On success, "200 OK" shall be returned. On failure, the appropriate error code (see annex B) shall be returned. + +## 6.7 Pattern: Queries on a resource + +## 6.7.1 Description + +This pattern influences the response of the GET method by passing resource URI parameters in the query part of the +resource URI. The syntax of the query part is specified by IETF RFC 3986 [9]. + +Typically, query parameters are used for: + +- restricting a set of objects to a subset, based on filtering criteria; +- controlling the content of the result; +- reducing the content of the result (such as suppressing optional attributes). + +``` +EXAMPLES: +``` +GET .../foo_list?vendor=MEC&ue_ids=ab1,cd2 + +``` + would return a foo_list representation that includes only those entries where vendor is "MEC" and the UE IDs +are "ab1" or "cd2". +``` +GET .../foo_list?group=group1 + +``` + would return a foo_list representation that includes only those entries that belong to "group1". +``` +GET .../foo_list/123?format=reduced_content + +``` + would return a representation of the resource .../foo_list/123 with content tailored according to the application- +specific "reduced_content" scope. +``` +GET .../foo_list?fields=name,address,key + +``` + would return a representation of the resource .../foo_list where the entries are reduced to the attributes "name", +"address" and "key". +``` +The list above provides just examples; the normative definition of individual simple queries and the related URI query +parameters are left to the actual API specifications. For a comprehensive query mechanism, the actual API +specifications can reference the mechanisms for attribute-based filtering and attribute selectors that are specified in +clauses 6.18 and 6.19 of the present document. + +Query values that are not compatible with URI syntax shall be escaped properly using percent encoding as defined in +IETF RFC 3986 [9]. + +## 6.7.2 Resource definition(s) and HTTP methods + +This pattern is applicable to any resource that can be read. The HTTP method shall be GET. + + +## 6.7.3 Resource representation(s) + +The payload body of the request shall be empty; the payload body of the response shall contain a representation of the +resource that was read, adjusted according to the parameters that were passed. + +## 6.7.4 HTTP headers + +No specific provisions for HTTP headers for this pattern. + +## 6.7.5 Response codes and error handling + +On success, "200 OK" shall be returned. On failure, the appropriate error code (see annex B) shall be returned. + +## 6.8 Pattern: Updating a resource (PUT) + +## 6.8.1 Description + +When a resource is updated using the PUT HTTP method, this operation has "replace" semantics. That is, the new state +of the resource is determined by the representation in the payload body of PUT, previous resource state is discarded by +the REST server when executing the PUT request. + +If the client intends to use the current state of the resource as the baseline for the modification, it is required to obtain a +representation of the resource by reading it, to modify that representation, and to place that modified representation in +the payload body of the PUT. If, on the other hand, the client intends to overwrite the resource without considering the +existing state, the PUT can be executed with a resource representation that is created from scratch. + +Figure 6.8.1-1 illustrates this flow. + +``` +Figure 6.8.1-1: Basic resource update flow with PUT +``` + +The approach illustrated above can suffer from race conditions. If another client modifies the resource after receiving +the response to the GET request and before sending the PUT request, that modification gets lost (which is known as the +lost update phenomenon in concurrent systems). HTTP (see IETF RFC 7232 [2]) supports conditional requests to detect +such a situation and to give the client the opportunity to deal with it. For that purpose, each version of a resource gets +assigned an "entity-tag" (ETag) that is modified by the server each time the resource is changed. This information is +delivered to the client in the "ETag" HTTP header in HTTP responses. If the client wishes that the server executes the +PUT only if the ETag has not changed since the time the GET response was generated, the client adds to the PUT +request the HTTP header "If-Match" with the ETag value obtained from the GET request. The server executes the PUT +request only if the ETag in the "If-Match" HTTP header matches the current ETag of the resource, and responds with +"412 Precondition Failed" otherwise. In that conflict case, the client needs to repeat the GET-PUT sequence. This is +illustrated in figure 6.8.1-2. + +``` +Figure 6.8.1-2: Resource update flow with PUT, considering concurrent updates +``` +In a particular API, it is recommended to stick to one update pattern - either PUT or PATCH. + +## 6.8.2 Resource definition(s) and HTTP methods + +This pattern is applicable to any resource that allows update by PUT. + +## 6.8.3 Resource representation(s) + +This pattern has no specific provisions for resource representations, other than the following note. + +``` +NOTE: Compared to the payload body passed in the request, the payload body in the response may be different, +as the resource update process may have modified the information that has been passed as input. +``` +## 6.8.4 HTTP headers + +If multiple clients can update the same resource, the client should pass in the "If-Match" HTTP header of the PUT +request the value of the "ETag" HTTP header received in the response to the GET request. + +``` +NOTE: This prevents the "lost update" phenomenon. +``` + +## 6.8.5 Response codes and error handling + +On success, either "200 OK" or "204 No Content" shall be returned. If the ETag value in the "If-Match" HTTP header +of the PUT request does not match the current ETag value of the resource, "412 Precondition Failed" shall be returned. +Otherwise, on failure, the appropriate error code (see annex B) shall be returned. + +Resource update can also be asynchronous in which case "202 Accepted" shall be returned instead of "200 OK". See +clause 6.13 for more details about asynchronous operations. + +## 6.9 Pattern: Updating a resource (PATCH) + +## 6.9.1 Description + +The PATCH HTTP method (see IETF RFC 5789 [3]) is used to update a resource on top of the existing resource state +with partial changes described by the client (unlike resource update using PUT which overwrites a resource (see +clause 6.8)). The "Update by PATCH" pattern can be used in all places where "Update by PUT" can be used, but is +typically more efficient for partially updating a large resource. + +As opposed to PUT, PATCH does not carry a representation of the resource in the payload body, but a "deltas +document" that instructs the server how to modify the resource representation. For JSON, JSON Patch (see IETF +RFC 6902 [6]) and JSON Merge Patch (IETF RFC 7396 [5]) are defined for that purpose. Whereas JSON Patch +declares commands that transform a JSON document, JSON Merge Patch defines fragments that are merged into the +target JSON document. For XML, a patch framework is specified in IETF RFC 5261 [7] which defines operations to +modify the target document. + +Figure 6.9.1-1 illustrates updating a resource by PATCH. + +``` +Figure 6.9.1-1: Basic resource update flow with PATCH +``` +Careful design of the PATCH payload can make the method idempotent, i.e. the order in which particular PATCH +operations are executed does not matter. If this can be achieved, the "lost update" phenomenon cannot occur. However, +if conflicts are possible, the If-Match HTTP header should be used in the same way as with PUT, as illustrated by +figure 6.9.1-2. + +``` +NOTE: Like in the PUT case, the ETag refers to the whole resource representation, not only to the portion +modified by the PATCH. +``` + +``` +Figure 6.9.1-2: Resource update flow with PATCH, considering concurrent updates +``` +In a particular API, it is recommended to stick to one update pattern - either PUT or PATCH. + +## 6.9.2 Resource definition(s) and HTTP methods + +This pattern is applicable to any resource that allows update by PATCH. + +## 6.9.3 Resource representation(s) + +The payload body of the PATCH request does not carry a representation of the resource, but a description of the +changes in one of the formats defined by IETF RFC 6902 [6], IETF RFC 7396 [5] and IETF RFC 5261 [7]. + +The payload body of the PATCH response may either be empty, or may carry a representation of the updated resource. + +An API specification should suitably specify which parts of the representation of a resource (objects in JSON and +elements in XML) are allowed to be modified by the PATCH operation. + +When using PATCH with JSON either JSON Patch [6] or JSON Merge Patch [5] are applicable: + +- JSON Patch addresses an object in the JSON representation of the resource to be changed using a JSON + Pointer (IETF RFC 6901 [4]) expression, and provides for that object an operation with the necessary + parameters to modify the object, delete the object, or insert a new object. The deltas document is a set of such + operations. JSON Patch is capable of expressing modifications to individual array elements. When specifying + the allowed modifications, a normative set of restrictions needs to be defined on the path expressions and the + operations on these. + + +- JSON Merge Patch uses a subset of the representation of the resource as the deltas document, where this + subset only carries the modified objects. The deltas document is a JSON file formatted the same way as the + representation of the resource. Objects that are not to be modified are omitted from the deltas document. JSON + Merge Patch is not capable of addressing individual array elements; which means that the deltas document + needs to contain a complete representation of the array with the changes applied, in case an array needs to be + modified. However, lists of objects that have an identifying attribute can be stored in a JSON map (list of + key-value-pairs) instead of in an array. The restriction of JSON Merge Patch for arrays does not apply to maps. + Therefore, using maps instead of arrays where applicable can make a data model design more JSON Merge + Patch friendly. The allowed modifications can simply be specified using the same format (tables, OpenAPI) as + for defining the data model for the resource representations. + +The APIs defined as part of the ETSI MEC specifications will use IETF RFC 7396 [5] when using PATCH with JSON. + +## 6.9.4 HTTP headers + +In the request, the "Content-type" HTTP header needs to be set to the content type registered for the format used to +describe the changes, according to IETF RFC 6902 [6], IETF RFC 7396 [5] or IETF RFC 5261 [7]. + +If conflicts and data inconsistencies are foreseen when multiple clients update the same resource, the client should pass +in the "If-Match" HTTP header of the PUT request the value of the "ETag" HTTP header received in the response to the +GET request. + +## 6.9.5 Response codes and error handling + +On success, either "200 OK" or "204 No Content" shall be returned. If the ETag value in the "If-Match" HTTP header +of the PATCH request does not match the current ETag value of the resource, "412 Precondition Failed" shall be +returned. Otherwise, on failure, the appropriate error code (see annex B) shall be returned. + +Resource update can also be asynchronous in which case "202 Accepted" shall be returned instead of "200 OK". See +clause 6.13 for more details about asynchronous operations. + +## 6.10 Pattern: Deleting a resource.............................................................................................................................. + +## 6.10.1 Description + +The Delete pattern deletes a resource by invoking the HTTP DELETE method on that resource. After successful +completion, the client shall not assume that the resource is available any longer. + +The response of the DELETE request is typically empty, but it is also possible to return the final representation of the +resource prior to deletion. + +When a deleted resource is accessed subsequently by any HTTP method, typically the server responds with "404 Not +Found", or, if the server maintains knowledge about the URIs of formerly-existing resources, "410 Gone". + +Figure 6.10.1-1 illustrates deleting a resource. + + +``` +Figure 6.10.1-1: Resource deletion flow +``` +## 6.10.2 Resource definition(s) and HTTP methods + +This pattern is applicable to any resource that can be deleted. The HTTP method shall be DELETE. + +## 6.10.3 Resource representation(s) + +The payload body of the request shall be empty. The payload body of the response is typically empty, but may also +include the final representation of the resource prior to deletion. + +## 6.10.4 HTTP headers + +No specific provisions for HTTP headers for this pattern. + +## 6.10.5 Response codes and error handling + +On success, "204 No Content" should be returned, unless it is the intent to provide the final representation of the +resource, in which case "200 OK" should be returned. On failure, the appropriate error code (see annex B) shall be +returned. + +If a deleted resource is accessed subsequently by any HTTP method, the server shall respond with "410 Gone" in case it +has information about the deleted resource available, or shall respond with "404 Not Found" in case it has no such +information. + +Resource deletion can also be asynchronous in which case "202 Accepted" shall be returned instead of "204 No +Content" or "200 OK". See clause 6.13 for more details about asynchronous operations. + + +## 6.11 Pattern: Task resources + +## 6.11.1 Description + +In REST interfaces, the goal is to use only four operations on resources: Create, Read, Update, Delete (the so-called +CRUD principle). However, in a number of cases, actual operations needed in a system design are difficult to model as +CRUD operations, be it because they involve multiple resources, or that they are processes that modify a resource, +taking a number of input parameters that do not appear in the resource representation. Such operations are modelled as +special URIs called "task resources". + +``` +NOTE: In strict REST terms, these URIs are not resources, but endpoints that are included in the resource tree +that represent specific non-CRUD operations. Therefore, these special URIs are also sometimes called +"custom operations". +``` +A task resource is a child resource of a primary resource which is intended as an endpoint for the purpose of invoking a +non-CRUD operation. That non-CRUD operation executes a procedure that modifies the state of the primary resource in +a specific way, or performs a computation and returns the result. Task resources are an escape means that allows to +incorporate aspects of a service-oriented architecture into a RESTful interface. + +The only HTTP method that is supported for a task resource is POST, with a payload body that provides input +parameters to the process which is triggered by the request. Different responses to a POST request to a task resource are +possible, such as "202 Accepted" (for asynchronous invocation), "200 OK" (to provide a result of a computation based +on the state of the resource and additional parameters), "204 No Content" (to signal success but not return a result), or +"303 See Other" to indicate that a different resource than the primary resource was modified. The actual code used +depends greatly on the actual system design. + +## 6.11.2 Resource definition(s) and HTTP methods + +A task resource that models an operation on a particular primary resource is often defined as a child resource of that +primary resource. The name of the resource should be a verb that indicates which operation is executed when sending a +POST request to the resource. + +``` +EXAMPLE: .../call_sessions/{sessionId}/call_participants/{participantId}/transfer. +``` +The HTTP method shall be POST. + +## 6.11.3 Resource representation(s) + +The payload body of the POST request does not carry a resource representation, but contains input parameters to the +process that is triggered by the POST request. + +## 6.11.4 HTTP headers + +In case the task resource represents an operation that is asynchronous, the provisions in clause 6.13 shall apply. + +In case the operation modifies a different resource than the primary resource and the response contains the "303 See +Other" response code, the "Location" HTTP header shall point to the modified resource. + +## 6.11.5 Response codes and error handling + +The response code returned depends greatly on the actual operation that is represented as a task resource, and may +include the following: + +- For long-running operations, "202 Accepted" is returned. See clause 6.13 for more details about asynchronous + operations. +- If the operation modifies another resource, "303 See Other" is returned. +- If the operation returns a computation result, "200 OK" is returned. +- If the operation returns no result, "204 No Content" is returned. + + +On failure, the appropriate error code (see annex B) shall be returned. + +## 6.12 Pattern: REST-based subscribe/notify + +## 6.12.1 Description + +A common task in distributed system is to keep all involved components informed of changes that appear in a particular +component at a particular time. A common approach to spread information about a change is to distribute notifications +about the change to those components that have indicated interest earlier on. Such pattern is known as Subscribe/Notify. +In REST which is request-response by design, meaning that every request is initiated by the client, specific mechanisms +needs to be put into place to support the server-initiated delivery of notifications. The basic principle is that the REST +client exposes a lightweight HTTP server towards the REST server. The lightweight HTTP server only needs to support +a small subset of the HTTP functionality - namely the POST method, the "204 No Content" success response code plus +the relevant error response codes, and, if applicable, authentication/authorization. The REST client exposes the +lightweight HTTP server in a way that it is reachable via TCP by the REST server. + +``` +NOTE: This clause describes REST-based subscribe/notify. Notifications can also be subscribed to and delivered +by an alternative transport mechanism, such as a message bus. There is a separate pattern for this, see +clause 7. +``` +To manage subscriptions, the REST server needs to expose a container resource under which the REST client can +request the creation/deletion of subscription resources. Those resources optionally define criteria of the subscription as +part of the resource URI (such as the "{subscriptionType}" example in figure 6.12.1-1). See clauses 6.5 and 6.10 for the +patterns of creating and deleting resources which apply to subscription resources as well. + +To receive notifications, the client exposes one or more HTTP endpoints on which it can receive POST requests. When +creating a subscription, the client shall inform the server of the endpoint to which the server will later deliver +notifications related to that particular subscription. The structure of the URI of that endpoint (aka callback URI) is +defined by the client, the string "evt_sink" in figure 6.12.1-1 is an example. + +To deliver notifications, the server includes the actual notification payload in the payload body of a POST request and +sends that request to the endpoint it knows from the subscription. The client acknowledges the receipt of the notification +with "204 No Content". + +Figure 6.12.1-1 illustrates the creation of subscriptions and the delivery of a notification. + +``` +Figure 6.12.1-1: Creation of subscriptions and delivery of a notification +``` + +Beyond this very basic scheme described above, the server may also allow the client to update subscriptions, and +subscriptions may carry an expiry deadline. Update shall be performed using PUT. In particular, when applying the +update operation, the REST client can modify the expiry deadline to refresh a subscription. If the server expires a +subscription, it sends an ExpiryNotification to the client's HTTP endpoint defined in the subscription. See clause 6.8 for +the pattern of updating a resource using PUT, which applies to the update of subscription resources as well. + +Once a subscription is expired, the subscription resource is not available anymore. + +Figure 6.12.1-2 illustrates a realization with update and expiry of subscriptions. + +``` +Figure 6.12.1-2: Management of subscriptions with expiry +``` +## 6.12.2 Resource definition(s) and HTTP methods + +The following resources are involved: + +``` +1) Subscriptions container resource: A resource that can hold zero or more subscription resources as child +resources. +``` +``` +2) Subscription resource: A resource that represents a subscription. +``` +``` +3) An HTTP endpoint that is exposed by the REST client to receive the notifications. +``` +The HTTP method to create a subscription resource inside the subscription container resource shall be POST. The +HTTP method to terminate a subscription by removing a subscription resource shall be DELETE. The HTTP method +used by the server to deliver the notification shall be POST. + +If update of subscriptions is supported, the HTTP method to perform the update shall be PUT. + +If expiry of subscriptions is supported, the delivery of an ExpiryNotification to the subscribed clients, and the update of +subscription resources should be supported to allow extension of the lifespan of a resource. + + +## 6.12.3 Resource representation(s) + +The following provisions are applicable to the representation of a subscription resource: + +- It shall contain a callback URI which addresses the HTTP endpoint that the REST client exposes to receive + notifications. The callback URI shall be in the form of an absolute URI as defined in section 4.3 of IETF + RFC 3986 [9] excluding any query component, any fragment component and any userinfo subcomponent. +- It should contain criteria that allow the server to determine the events about which the client wishes to be + notified. +- If expiry of subscriptions is supported, it shall contain an expiry time after which the subscription is no longer + valid, and no notifications will be generated for it. + +If subscription expiry is supported, the following provisions are applicable to the representation of an +ExpiryNotification: + +- It shall contain a reference to the subscription that has expired. +- It may contain information about the reason of the expiry. + +The following provisions are applicable to the representation of any other notification: + +- It should contain a reference to the related subscription. +- It shall contain information about the event. + +## 6.12.4 HTTP headers + +No specific provisions are applicable here. + +## 6.12.5 Response codes and error handling + +The response codes for subscription creation, subscription deletion, subscription read and subscription update are the +same as for the general resource creation, resource deletion, resource read and resource update. + +On success of notification delivery, "204 No Content" shall be returned. + +On failure, the appropriate error code (see annex B) shall be returned. + +If expiry of subscriptions is supported: Once an expiry notification has been delivered to the client, any HTTP request +to the expired subscription resource shall fail. For a timespan determined by policy or implementation, "410 Gone" is +recommended to be used as the response code in that case, and "404 Not Found" shall be used afterwards. + +``` +NOTE: In order to be able to respond with "410 Gone", the server needs to keep information about the expired +subscription. +``` +## 6.12a Pattern: REST-based subscribe/notify with Websocket fallback + +## 6.12a.1 Description + +REST-based delivery of notifications as defined in clause 6.12 might not work in case middleboxes block the HTTP +connection attempts to send the POST requests that deliver the notifications. An example where a NAT middlebox +blocks the notification delivery over HTTP is illustrated in figure 6.12a.1-1. + + +``` +Figure 6.12a.1-1: NAT as an example of a middlebox blocking notification delivery over HTTP +``` +In order to cope with such network topologies, the direction of setting up the TCP connection through which the +notifications are sent needs to be reversed, and this needs to be done in an HTTP-proxy compatible way. Websockets +(see IETF RFC 6455 [25]) provide a solution that fulfils these requirements. + +``` +Figure 6.12a.1-2: Topology probing and notification delivery mechanism negotiation +``` + +Figure 6.12a.1-2 illustrates the steps of topology probing during subscription, and the following negotiation of the +Websocket URI in case of HTTP transport of notifications is blocked. This mechanism is aligned with 3GPP T8 (see +ETSI TS 129 122 [26] and re-used by CAPIF (see ETSI TS 129 222 [i.15]). + +``` +1) The client subscribes to notifications by sending a POST request to the container resource that holds the +subscriptions, providing in the request body a callback URI (e.g. ".../evt_sink") and a flag to request a test +notification. +``` +``` +2) The server creates a subscription resource and informs the client about the creation of that resource. +``` +``` +3) The server sends to the callback URI that was indicated in the subscription a POST request and includes in the +payload body a test notification. +``` +In case the test notification is received by the client, steps 4-6 are executed: + +``` +4) The client confirms with "204 No Content" that the test notification was received. +``` +The following steps 5 and 6 are executed in a loop for each event that triggers a notification: + +``` +5) The server sends a POST request to the callback URI and includes in the payload body a notification structure +related to the event. +``` +``` +6) The client confirms with "204 No Content" that the notification was received. +``` +In case the test notification is not received by the client before a time-out, steps 7-11 are executed: + +``` +7) The client sends a PUT request to the subscription resource to update the notification delivery method to +Websockets. +``` +``` +8) The server provides a Websocket endpoint to which the client will subsequently connect to set up a Websocket +connection. Further, the server includes the URI of that endpoint in the representation of the subscription +resource and returns a "200 OK" response. +``` +``` +9) The client opens to the provided Websocket endpoint a Websocket connection through which the server can +subsequently push notifications. +``` +The following steps 10 and 11 are executed in a loop for each event that triggers a notification: + +``` +10) The server sends to the client, via the Websocket connection, a notification structure related to the event with +the appropriate framing. +``` +``` +11) The client confirms the delivery to the server, via the Websocket connection, with a "204" response code in the +appropriate framing. +``` +In case neither the 204 response (step 4) nor the subscription update (step 7) is received by the server within an +implementation-specific time interval, the server should retry sending the test notification. The behaviour of the server +in case of multiple subsequent failures is out of the scope of the present document. + +## 6.12a.2 Resource definition(s) and HTTP methods + +The resources defined in clause 6.12.2 and the following are involved: + +``` +1) A Websocket endpoint that is exposed by the REST server to establish a Websocket connection for +notifications delivery. +``` +The HTTP methods are as defined in clause 6.12.2, with the following addition: The PUT method to update the +subscription shall be supported in order to enable updating the delivery mechanism. + +In addition to the HTTP methods, the delivery of notifications from the server to the client through a Websocket +connection shall be supported. Such delivery shall use the framing that is defined in clause 5.2.5.4 of ETSI +TS 129 122 [26], where the REST client corresponds to the SCS/AS and the REST server corresponds to the SCEF. For +that purpose, the establishment of a Websocket connection by the REST client towards a Websocket endpoint provided +by the REST server shall be supported. + + +## 6.12a.3 Resource representation(s) + +``` +The provisions defined in clause 6.12.3 for the representation of a subscription resource apply. +``` +``` +In addition, the following shall be supported: +``` +``` +1) Specific attributes in the subscription data structure to negotiate and signal the use of Websockets +``` +``` +The representation of a subscription resource shall include the attributes defined in table 6.12a.3-1. +``` +``` +Table 6.12a.3-1: Definition of subscription attributes for Websocket negotiation and signaling +``` +Attribute name Data type Cardinality Description +callbackUri Uri 0..1 URI exposed by the client on which to receive notifications via +HTTP. See note. +requestTestNotification Boolean 0..1 Shall be set to TRUE in a request to create a subscription if the +client intends to receive a test notification via HTTP on the +callbackUri that it provides in the same subscription request. +Default: FALSE. +websockNotifConfig WebsockNoti +fConfig + +0..1 Provides details to negotiate and signal the use of a +Websocket connection, as defined in clause 5.2.1.2.10-1 of +ETSI TS 129 122 [26]. The server may assign the same +websocket URI to multiple subscriptions of the same client. +See note. +... ... ... (Additional attributes of the subscription data structure) +NOTE: At least one of callbackUri and websocketNotifConfig shall be provided in any subscription. If both are +provided, it is up to the server to choose an alternative and return only that alternative in the response. + +``` +2) A test notification payload +``` +``` +The test notification shall include the attributes defined in clause 5.2.1.2.9 of ETSI TS 129 122 [26]. It may +include additional attributes. +``` +``` +It shall be sent via HTTP by the REST server to the callback URI provided by the REST client upon +subscription, if the REST client has indicated in the subscription the flag "requestTestNotification=true", +according to clause 5.2.5.3 of ETSI TS 129 122 [26]. +``` +## 6.12a.4 HTTP headers + +``` +No specific provisions are applicable here. +``` +## 6.12a.5 Response codes and error handling + +``` +The same provisions as in clause 6.12.5 apply. +``` +## 6.13 Pattern: Asynchronous operations + +## 6.13.1 Description + +``` +Certain operations, which are invoked via a RESTful interface, trigger processing tasks in the underlying system that +may take a long time, from minutes over hours to even days. In this case, it is inappropriate for the REST client to keep +the HTTP connection open to wait for the result of the response - the connection will time out before a result is +delivered. For these cases, asynchronous operations are used. The idea is that the operation immediately returns the +provisional response "202 Accepted" to indicate that the request was understood, can be correctly marshalled in, and +processing has started. The client can check the status of the operation by polling; additionally, or alternatively, the +subscribe-notify mechanism (see clause 6.12) can be used to provide the result once available. The progress of the +operation is reflected by a monitor resource. +``` + +Figure 6.13.1-1 illustrates asynchronous operations with polling. After receiving an HTTP request that is to be +processed asynchronously, the server responds with "202 Accepted" and includes in the payload body or in a specific +"Link" HTTP header a data structure that points to a monitor resource which represents the progress of the processing +operation. The client can then poll the monitor resource by using GET requests, each returning a data structure with +information about the operation, including the processing status such as "processing", "success" and "failure". Initially, +the status is set to "processing". Eventually, when the processing is finished, the status is set to "success" (for successful +completion of the operation) or "failure" (for completion with errors). Typically, the representation of a monitor +resource will include additional information, such as information about an error if the operation was not successful. + +``` +Figure 6.13.1-1: Asynchronous operation flow - with polling +``` +Figure 6.13.1-2 illustrates asynchronous operations with subscribe/notify. Before a client issues any request that may be +processed asynchronously, it subscribes for monitor change notifications. Later, after receiving an HTTP request that is +to be processed asynchronously, the server responds with "202 Accepted" and includes in the payload body or in a +specific "Link" HTTP header a data structure that points to a monitor resource which represents the progress of the +processing operation. The client can now wait for receiving a notification about the operation finishing, which will +change the status of the monitor. Once the operation is finished, the server will send to the client a notification with a +structure in the payload body that typically includes the status of the operation (e.g. "success" or "failure"), a link to the +actual monitor affected, and a link to the resource that is modified by the asynchronous operation, The client can then +poll the monitor to obtain further information. + + +``` +Figure 6.13.1-2: Asynchronous operation flow - with subscribe/notify +``` +## 6.13.2 Resource definition(s) and HTTP methods + +The following resources are involved: + +``` +1) Primary resource: The resource that is about to be created/modified/deleted by the long-running operation. +``` +``` +2) Monitor resource: The resource that provides information about the long-running operation. +``` +The HTTP method applied to the primary resource can be any of POST/PUT/PATCH/DELETE. + +The HTTP method applicable to read the monitor resource shall be GET. + +If monitor change notifications and subscriptions to these are supported, the resources and methods described in +clause 6.12 for the RESTful subscribe/notify pattern are applicable here too. + +## 6.13.3 Resource representation(s) + +If present, the structure included in the payload body of the response to the long-running operation request shall contain +the resource URI of the monitor for the operation, and shall also contain the resource URI of the actual primary +resource. See clause 6.14 for further information on links. If no payload body is present, the "Link" HTTP header shall +be used to convey the link to the monitor. + +The representation of the monitor shall contain at least the following information: + +- Resource URI of the primary resource. +- Status of the operation (at least "processing", "success", "failure"). +- Additional information about the result or the error(s) occurred, if applicable. +- Information about the operation (type, parameters, HTTP method used). + +If subscribe/notify is supported, the monitor change notification shall include the status of the operation and the +resource URI of the monitor, and shall include the resource URI of the affected primary resource. + + +## 6.13.4 HTTP headers + +The link to the monitor should be provided in the "Link" HTTP header (see IETF RFC 8288 [12]), with the "rel" +attribute set to "monitor". If the payload body of the message is not present, the "Link" as defined above shall be +provided. + +``` +EXAMPLE: Link: ; rel="monitor". +``` +## 6.13.5 Response codes and error handling + +On success, "202 Accepted" shall be returned as the response to the request that triggers the long-running operation. On +failure, the appropriate error code (see annex B) shall be returned. + +The GET request to the monitor resource shall use "200 OK" as the response code if the monitor could be read +successfully, or the appropriate error code (see annex B) otherwise. + +If subscribe/notify is supported, the provisions in clause 6.12.5 apply in addition. + +## 6.14 Pattern: Links (HATEOAS) + +## 6.14.1 Description + +The REST maturity level 3 requires the use of links between resources, allowing the REST client to traverse the +resource space. ETSI MEC recommends using level 3. This is also known as "hypermedia controls" or "HATEOAS" +(hyperlinks as the engine of application state). This clause describes a pattern for hyperlinks. + +Hyperlinks to other resources should be embedded into the representation of resources where applicable. For each +hyperlink, the target URI of the link and information about the meaning of the link shall be provided. Knowing the +meaning of the link (typically conveyed by the name of the object that defines the link, or by an attribute such as "rel") +allows the client to automatically traverse the links to access resources related to the actual resource, in order to perform +operations on them. + +## 6.14.2 Resource definition(s) and HTTP methods + +Links can be applicable to any resource and any HTTP method. + +## 6.14.3 Resource representation(s) + +Links are communicated in the resource representation. Links that occur at the same level in the representation shall be +bundled in an object (JSON) or element containing complexContent (XML schema), named "_links" which should +occur as the first object/element at a particular level. + +Links shall be embedded in that element (XML) or object (JSON) as child elements (XML) or contained objects +(JSON). The name of each child element (XML) or contained object (JSON) defines the semantics of the particular +link. The content of each link element/object shall be an attribute named "href" of type "anyURI" (XML) or an object +named "href" of type string (JSON), which defines the target URI the link points to. The link to the actual resource shall +be named "self" and shall be present in every resource representation if links are used in the API. + +As an example, the "_links" portion of a resource representation is shown that represents paged information. + +For the case of using XML, figure 6.14.3-1 illustrates the XML schema and figure 6.14.3-2 illustrates the XML +instance. The XML schema language is defined in [i.5]. + + + + + + + + + + + + + + + + +``` +Figure 6.14.3-1: XML schema fragment for an example "_links" element +``` +<_links> + + + + + +``` +Figure 6.14.3-2: XML instance fragment for an example "_links" element +``` +For the case of using JSON, figure 6.14.3-3 illustrates the JSON schema and figure 6.14.3-4 illustrates the JSON object. +The JSON schema language is defined in [i.4]. + +{ +"properties": { +"_links": { +"required": ["self"], +"type": "object", +"description": "Link relations", +"properties": { +"self": { +"$ref": "#/definitions/Link" +}, +"prev": { +"$ref": "#/definitions/Link" +}, +"next": { +"$ref": "#/definitions/Link" +} +} +} +}, +"definitions": { +"Link" : { +"type": "object", +"properties": { +"href": {"type": "string"} +}, +"required": ["href"] +} +} +} + +``` +Figure 6.14.3-3: JSON schema fragment for an example "_links" object +``` +{ +"_links": { +"self": { "href": "http://api.example.com/my_api/v1/pages/127" }, +"next": { "href": "http://api.example.com/my_api/v1/pages/128" }, +"prev": { "href": "http://api.example.com/my_api/v1/pages/126" } +} +} + +``` +Figure 6.14.3-4: JSON fragment for an example "_links" object +``` + +## 6.14.4 HTTP headers + +There are no specific provisions with respect to HTTP headers for this pattern. + +``` +NOTE: Specific links, such as a link to the monitor in a "202 Accepted" response, can be communicated in the +"Link" HTTP header. See clause 6.13 for more details. +``` +## 6.14.5 Response codes and error handling + +There are no specific provisions with respect to response codes and error handling for this pattern. + +## 6.15 Pattern: Error responses + +## 6.15.1 Description + +In RESTful interfaces, application errors are mapped to HTTP errors. Since HTTP error information is typically not +enough to discover the root cause of the error, there is the need to deliver additional application specific error +information. + +When an error occurs that prevents the REST server from successfully fulfilling the request, the HTTP response +includes a status code in the range 400..499 (client error) or 500..599 (server error) as defined by the HTTP +specification (see IETF RFC 7231 [1] and IETF RFC 6585 [8]). In addition, to provide additional application-related +error information, the present document recommends the response body to contain a representation of a +"ProblemDetails" data structure according to IETF RFC 7807 [15] that provides additional details of the error. + +## 6.15.2 Resource definition(s) and HTTP methods + +The pattern is applicable to the responses of all HTTP methods. + +## 6.15.3 Resource representation(s) + +If an HTTP response indicates non-successful completion (error codes 400..499 or 500..599), the response body should +contain a "ProblemDetails" data structure as defined below, formatted using the same format as the expected response. +The response body may be omitted if the HTTP error code itself provides enough information of the error, or if there +are security concerns disclosing detailed error information. + +The definition of the general "ProblemDetails" data structure from IETF RFC 7807 [15] is reproduced in table 6.15.3-1. +Compared to the general framework in IETF RFC 7807 [15] where the "status" and "detail" attributes are recommended +to be included, these attributes shall be included when this data structure is used in the context of the ETSI MEC REST +APIs, to ensure that the response contains additional textual information about an error. IETF RFC 7807 [15] foresees +extensibility of the "ProblemDetails" type. It is possible that particular APIs or particular implementations define +extensions to define additional attributes that provide more information about the error. + +The description column only provides some explanation of the meaning to facilitate understanding of the design. For a +full description, see IETF RFC 7807 [15]. + + +``` +Table 6.15.3-1: Definition of the ProblemDetails data type +``` +Attribute name Data type Cardinality Description +type Uri 0..1 A URI reference according to IETF RFC 3986 [9] that identifies +the problem type. It is encouraged that the URI provides +human-readable documentation for the problem (e.g. using +HTML) when dereferenced. When this member is not present, +its value is assumed to be "about:blank". See note 1. +title String 0..1 A short, human-readable summary of the problem type. It +should not change from occurrence to occurrence of the +problem, except for purposes of localization. If type is given +and other than "about:blank", this attribute shall also be +provided. +status Integer 1 +(see note 2) + +The HTTP status code for this occurrence of the problem. See +note 3. +detail String 1 +(see note 2) + +A human-readable explanation specific to this occurrence of +the problem. +instance Uri 0..1 A URI reference that identifies the specific occurrence of the +problem. It may yield further information if dereferenced. +(additional attributes) Not specified 0..N Any number of additional attributes, as defined in a +specification or by an implementation. +NOTE 1: For the definition of specific "type" values as well as extension attributes by implementations, detailed +guidance can be found in IETF RFC 7807 [15]. +NOTE 2: In IETF RFC 7807 [15], the "status" and "detail" are recommended which would translate into a cardinality of +0..1, but the present document requires the presence of these attributes as the minimum set of information +returned in ProblemDetails. +NOTE 3: IETF RFC 7807 [15] requires that this attribute duplicates the value of the status code in the HTTP response +message. See section 5 of IETF RFC 7807 [15] for guidance related to the two values differing. + +## 6.15.4 HTTP headers + +``` +As defined by IETF RFC 7807 [15]: +``` +- In case of serializing the "ProblemDetails" structure using the JSON format, the "Content-Type" HTTP header + shall be set to "application/problem+json". +- In case of serializing the "ProblemDetails" structure using the XML format, the "Content-Type" HTTP header + shall be set to "application/problem+xml". + +## 6.15.5 Response codes and error handling + +``` +In general, application errors should be mapped to the most similar HTTP error status code. If no such code is +applicable, one of the codes 400 (Bad request, for client errors) or 500 (Internal Server Error, for server errors) should +be used. +``` +``` +Implementations may use any valid HTTP response code as error code in the HTTP response, but shall not use any code +that is not a valid HTTP response code. A list of all valid HTTP response codes and their specification documents can +be obtained from the HTTP status code registry [i.8]. Annex B lists a set of error codes that is frequently used in +HTTP-based RESTful APIs. Annex E provides more detail on common error situations. +``` +## 6.16 Pattern: Authorization of access to a RESTful MEC service API using OAuth 2.0 + +## 6.16.1 Description + +``` +This pattern defines the use of OAuth 2.0 to secure a RESTful MEC service API. It is used for the RESTful APIs that +are defined by ETSI ISG MEC. Service-producing applications defined by third parties may use other mechanisms to +secure their APIs, such as standalone use of JWT (see IETF RFC 7519 [i.13]). +``` + +The API security framework assumes an AA (authentication and authorization) entity to be available for both the REST +client and the REST server. This AA entity performs the authentication for the credentials of the REST clients and the +REST servers. The AA entity and the communication between the REST server and the AA entity are out of scope of +the present document. + +It is assumed that the AA entity is configured by a trusted Manager entity with the appropriate credentials and access +rights information. This configuration information is exchanged between the AA entity and the REST server in an +appropriate manner to allow the REST server to enforce the access rights. The trusted Manager and the actual way of +performing the exchange of this information are out of scope. + +The exchanges between REST client and REST server are in scope of the present document. The REST client has to +authenticate towards the AA entity in order to obtain an access token. Subsequently, the client shall present the access +token to the REST server with every request in order to assert that it is allowed to access the resource with the particular +method it invokes. In the present version of the specification, the client credentials grant type of OAuth 2.0 (see IETF +RFC 6749 [16]) shall be supported by the AA entity, and it shall be used by the REST client to obtain the access token. +In any HTTP request to a resource, the access token shall be included as a bearer token according to IETF +RFC 6750 [17]. + +Access rights are bound to access tokens, and typically configured at the granularity of methods applied to resources. +This means, for any resource in the API, the use of every individual method can be allowed or disallowed. In APIs that +define a REST-based subscribe-notify pattern, also the use of individual subscription types can be allowed or prohibited +by access rights. Additional policies can be bound to access tokens too, such as the frequency of API calls. A token has +a lifetime after which it is invalid. Depending on how the AA communicates with the REST server, it can also be +possible to revoke a token before it expires. + +Figure 6.16.1-1 illustrates the information flow between the three actors involved in securing the REST-based service +API, the REST client, the AA entity and the REST server. Dotted lines indicate exchanges that are out of scope of the +present document. It is assumed that information about the valid access tokens, such as expiry time, related client +identity, client access rights, scope values, optional revocation information, need to be made available by the AA entity +to the REST server by means outside the scope of the present document. + +The AA entity exposes the "token endpoint" as defined by OAuth 2.0. + + +``` +Figure 6.16.1-1: Securing a RESTful MEC service API with OAuth 2.0 +``` +The flow consists of the following steps: + +``` +1) The manager registers the REST client application with the AA entity and configures the permissions of the +application. The method for this is out of scope of the present document. +``` + +``` +2) The REST client sends an HTTP request to the REST server to access a resource. +``` +``` +3) The REST server responds with "401 Unauthorized" which indicates to the client that it has to obtain an access +token for access to the resource. +``` +``` +4) The REST client sends an access token request to the token endpoint provided by the AA entity as specified by +IETF RFC 6749 [16], and authenticates towards the AA entity with its client credentials. +``` +``` +5) The AA entity provides the token and additional configuration information to the REST client, as specified by +IETF RFC 6749 [16]. +``` +``` +6) The REST client repeats the request from step (2) with the access token included as a bearer token according +to IETF RFC 6750 [17]. +``` +``` +7) The REST server checks the token for validity, and determines whether the client is authorized to perform the +request. This assumes that the REST server has received from the AA entity information about the valid access +tokens, and additional related parameters (e.g. expiry time, client identity, client access rights, scope values). +Exchange of such information is outside the scope of the present document, and is assumed to be trivial if +deployments choose to include the AA entity as a component into the REST server. +``` +``` +8) In case the client is authorized, the REST server executes the HTTP request and returns an appropriate HTTP +response rather than a "401 Unauthorized" error. +``` +``` +9) In case the client is not authorized, the REST server returns a "401 Unauthorized" error as defined in IETF +RFC 6750 [17]. +``` +``` +10) The REST client sends to the REST server an HTTP request with an expired token. +``` +``` +11) The REST server checks the token for validity, and establishes that it has expired. This assumes that the REST +server has previously received information about the valid access tokens, and additional related information (in +particular, the time of expiry) from the AA entity. Exchange of such information is outside the scope of the +present document, and is assumed to be trivial if deployments choose to include the AA entity as a component +into the REST server. +``` +``` +12) The REST server responds with "401 Unauthorized", and uses the format defined in IETF RFC 6750 [17] to +communicate that the access token is expired. +``` +``` +13) The REST client sends a new access token request to the AA entity, as defined in step (4). Subsequently, +steps (5) to (9) repeat. +``` +Optionally: + +``` +14) The REST client sends to the REST server an HTTP request with a revoked token. For this optional sequence, +it is assumed that the Manager has arranged to block an application from accessing a particular resource or set +of resources, or has changed the application's access rights prior to that request. By means outside the scope of +the present document, the Manager has further informed the AA entity about this change. +``` +``` +15) The REST server checks the token for validity, and establishes that it has been revoked. This assumes that the +REST server has previously received information about the validity of the access token from the AA entity. +Exchange of such information is outside the scope of the present document, and is assumed to be trivial if +deployments choose to include the AA entity as a component into the REST server. +``` +``` +16) The REST server responds with "401 Unauthorized". Eventually, the REST client can succeed with another +subsequent access token request if the revocation only affected a subset of the resources. +``` +## 6.16.2 Resource definition(s) and HTTP methods + +The HTTP methods follow the corresponding RESTful MEC service API definitions. Typically, when configuring the +AA entity, access rights can be expressed separately for each resource and HTTP method. In case subscriptions are +supported, separate access rights can also be defined per subscription data type. + + +## 6.16.3 Resource representation(s) + +The representation of the information exchanged between the REST client and the Token endpoint of the AA entity +shall follow the provisions defined in IETF RFC 6749 [16] for the client credentials grant type. The representation of +information exchanged between the Manager and the AA entity, as well as between the AA entity and the REST server, +are outside the scope of the present document. + +## 6.16.4 HTTP headers + +In this pattern, the access token is provided as defined by IETF RFC 6750 [17]. To protect the access token from +wiretapping, HTTPS shall be used. + +## 6.16.5 Response codes and error handling + +The response codes on the API between the REST server and the REST client are defined in the corresponding RESTful +MEC service API definitions, and shall include the provisions in IETF RFC 6750 [17]. The response codes on the token +endpoint provided by the AA entity shall follow IETF RFC 6749 [16]. + +## 6.16.6 Discovery of the parameters needed for exchanges with the token endpoint + +In order to be able to communicate with the token endpoint for a particular API, the REST client needs to discover its +URI. The valid scope values (if supported) are part of the API documentation. The client further needs to know which +set of client credentials to use to access the token endpoint. The token endpoint URI and the optional scope values will +be provided as part of the security information during service discovery. The client credentials consist of the client +identifier which is defined based on information in the application descriptor such as the values of the attributes +"appProvider" and "appName", and the client password which is provisioned during application on-boarding, and +configured into the client and the AA entity by means outside the scope of the present document. + +## 6.16.7 Scope values + +OAuth 2.0 (IETF RFC 6749 [16]) supports the concept of scope values to signal which actual access rights a token +represents. The scope of the token can be requested by the client in the access token request by listing one or more +scope values in the "scope" parameter. The AA entity can then potentially downscope the request, and respond with the +actual scope(s) represented by an access token in the access token response in the "scope" parameter. The use of scopes +is optional in OAuth 2.0. Per API, valid scopes can be defined in the API specification. Possible granularities are +resources, combinations of resources and methods, or even combinations of resources and methods with actual +parameter values, or values of attributes in the payload body. If no scope is defined, an access token always applies to +all resources and methods of a particular API. For a REST API using OAuth 2.0, the "permission identifiers" as defined +in clause 7.2 can be modelled as scope values, as illustrated in table 7.2-3. It is good practice to define one additional +scope value per API that includes all individual access rights, for simplification of use. + +## 6.17 Pattern: Representation of lists in JSON + +## 6.17.1 Description + +Lists of objects in JSON can be represented in two ways: arrays and maps. + +## 6.17.2 Representation as arrays + +A JSON array represents a list of objects as an ordered sequence of JSON objects. The order is significant; each object +in the array can be addressed by its position, i.e. its index. + +When modifying an array with PATCH (see clause 6.9), modifications can be represented by passing only the changes +when using the JSON Patch (IETF RFC 6902 [6]) format for the delta document, or by passing the whole modified +array when using the JSON Merge Patch (IETF RFC 7396 [5]) format for the delta document. + + +Figure 6.17.2-1 provides an example of a list of objects represented as JSON array. + +{ +"persons": [ +{"id": "123", "name": "Alice", "age": 30}, +{"id": "abc", "name": "Bob", "age": 40} +] +} + +``` +Figure 6.17.2-1: Example of an array of JSON objects +``` +## 6.17.3 Representation as maps + +A JSON map represents a list of objects as an associative set of key-value pairs (where each value is a JSON object). +The order of the entries in the map is not significant; each object in the map can be addressed by its unique key. +Representation as map requires that the objects in the list have an identifying property, i.e. an attribute with unique +values, such as an identifier. That attribute is used as key in the map. + +When modifying a map with PATCH (see clause 6.9), modifications can be represented by passing only the changes +when using either of the JSON Patch (IETF RFC 6902 [6]) format or the JSON Merge Patch (IETF RFC 7396 [5]) +format for the delta document. + +Figure 6.17.3-1 provides an example of a list of objects represented as JSON map, using the same data as in +figure 6.17.2-1. + +{ +"persons": { +"123": {"name": "Alice", "age": 30}, +"abc": {"name": "Bob", "age": 40} +} +} + +``` +Figure 6.17.3-1: Example of a map of JSON objects +``` +## 6.18 Pattern: Attribute selectors + +## 6.18.1 Description + +Certain resource representations can become quite big, in particular, if the resource is a container for multiple +sub-resources, or if the resource representation itself contains a deeply-nested structure. In these cases, it can be desired +to reduce the amount of data exchanged over the interface and processed by the client application. + +An attribute selector, which is typically part of a query, allows the client to choose which attributes it wants to be +contained in the response. Only attributes that are not required to be present, i.e. those with a lower bound of zero on +their cardinality (e.g. 0..1, 0..N) and that are not conditionally mandatory, are allowed to be omitted as part of the +selection process. Attributes can be marked for inclusion or exclusion. + +## 6.18.2 Resource definition(s) and HTTP methods + +The pattern is applicable to GET methods on specific resources. The applicability of attribute selectors is specified in +the API specifications per resource. + +The attribute selector is represented using URI query parameters, as defined in table 6.18.2-1. + +In the provisions below, "complex attributes" are assumed to be those attributes that are structured or that are arrays. + + +``` +Table 6.18.2-1: Attribute selector parameters +``` +``` +Parameter Definition +all_fields This URI query parameter requests that all complex attributes are included in the response, +including those suppressed by exclude_default. It is inverse to the "exclude_default" +parameter. +fields This URI query parameter requests that only the listed complex attributes are included in +the response. +The parameter shall be formatted as a list of attribute names. An attribute name shall either +be the name of an attribute, or a path consisting of the names of multiple attributes with +parent-child relationship, separated by "/". Attribute names in the list shall be separated by +comma (","). Valid attribute names for a particular GET request are the names of all +complex attributes in the expected response that have a lower cardinality bound of 0 and +that are not conditionally mandatory. +exclude_fields This URI query parameter requests that the listed complex attributes are excluded from the +response. For the format and eligible attributes, the provisions defined for the "fields" +parameter shall apply. +exclude_default Presence of this URI query parameter requests that a default set of complex attributes shall +be excluded from the response. The default set is defined per resource in the API +specification. Not every resource will necessarily have such a default set. Only complex +attributes with a lower cardinality bound of zero that are not conditionally mandatory can be +included in the set. +This parameter is a flag, i.e. it has no value. +``` +The "/" and "~" characters in attribute names in an attribute selector shall be escaped according to the rules defined in +section 3 of IETF RFC 6901 [4]. The "," character in attribute names in an attribute selector shall be escaped by +replacing it with "~a". Further, percent-encoding as defined in IETF RFC 3986 [9] shall be applied to the characters that +are not allowed in a URI query part according to Appendix A of IETF RFC 3986 [9], and to the ampersand "&" +character. + +Support of the attribute selector parameters can be defined per API. Only resources that represent a list of items and that +support a GET request are candidates for supporting attribute selectors. It can be decided in the API design if all such +resources actually need to support attribute selectors. Typically, list resources with items that have many and/or +complex attributes benefit from support of selectors, whereas for list resources with items that have only a few simple +attributes, support of attribute selectors can be overhead with no benefit. + +For each resource, it needs to be specified which attribute selector parameters are mandatory to support by the server, +and which ones are optional to support by the server. Use of these parameters is typically optional for the client. +Support for all_fields only makes sense if exclude_default is supported as well. There are two possible default values +for attribute selectors: "all_fields" and "exclude_default". + +The "all_fields" value is recommended to be used as default when the goal is to represent a list of items the same way as +the individual items, i.e. including all attributes, and if the response lists typically are short. For long lists and many +complex or array-type attributes, this default can result in large response data volumes. + +The "exclude_default" value is recommended to be used as default when the goal is to create a list that is a digest of the +individual items. This way, detailed information can be omitted, and the size of the response can be reduced. If the +individual list items contain a "self" link (see clause 6.14.3), it is recommended that this link is included in the response +list, as this facilitates easy drilldown on individual list items using subsequent GET requests. + +## 6.18.3 Resource representation(s) + +Table 6.18.3-1 defines the valid parameter combinations in a GET request and their effect on the response body. + + +``` +Table 6.18.3-1: Effect of valid combinations of attribute selector parameters on the response body +``` +``` +Parameter +combination +``` +``` +The GET response body shall include... +``` +``` +(none) ... same as "exclude_default". +all_fields ... all attributes. +fields= ... all attributes except all complex attributes with minimum cardinality of zero that are not +conditionally mandatory, and that are not provided in . +exclude_fields= ... all attributes except those complex attributes with a minimum cardinality of zero that are +not conditionally mandatory, and that are provided in . +exclude_default ... all attributes except those complex attributes with a minimum cardinality of zero that are +not conditionally mandatory, and that are part of the "default exclude set" defined in the API +specification for the particular resource. +exclude_default and +fields= +``` +``` +... all attributes except those complex attributes with a minimum cardinality of zero that are +not conditionally mandatory and that are part of the "default exclude set" defined in the API +specification for the particular resource, but that are not part of . +``` +## 6.18.4 HTTP headers + +There are no specific HTTP headers for this pattern. + +## 6.18.5 Response codes and error handling + +In case of success, the response code 200 OK shall be returned. + +In case of an invalid attribute selector, "400 Bad Request" shall be returned, and the response body shall contain a +ProblemDetails structure, in which the "detail" attribute should convey more information about the error. + +## 6.19 Pattern: Attribute-based filtering + +## 6.19.1 Description + +Attribute-based filtering allows to reduce the number of objects returned by a query operation. Typically, attribute- +based filtering is applied to a GET request that queries a resource which represents a list of objects (e.g. child +resources). Only those objects that match the filter are returned as part of the resource representation in the payload +body of the GET response. + +Attribute-based filtering can test a simple (scalar) attribute of the resource representation against a constant value, for +instance for equality, inequality, greater or smaller than, etc. Attribute-based filtering is requested by adding a set of +URI query parameters, the "attribute-based filtering parameters" or "filter" for short, to a resource URI. + +The following example illustrates the principle. Assume a resource "container" with the following objects: + +``` +EXAMPLE 1: Objects +obj1: {"id":123, "weight":100, "parts":[{"id":1, "color":"red"}, {"id":2, "color":"green"}]} +obj2: {"id":456, "weight":500, "parts":[{"id":3, "color":"green"}, {"id":4, "color":"blue"}]} +``` +A GET request on the "container" resource would deliver the following response: + +``` +EXAMPLE 2: Unfiltered GET +Request: +GET .../container +Response: +[ +{"id":123, "weight":100, "parts":[{"id":1, "color":"red"}, {"id":2, "color":"green"}]}, +{"id":456, "weight":500, "parts":[{"id":3, "color":"green"}, {"id":4, "color":"blue"}]} +] +``` + +A GET request with a filter on the "container" resource would deliver the following response: + +``` +EXAMPLE 3: GET with filter +``` +``` +Request: +GET .../container?filter=(eq,weight,100) +Response: +[ +{"id":123, "weight":100, "parts":[{"id":1, "color":"red"}, {"id":2, "color":"green"}]} +] +``` +For hierarchically-structured data, filters can also be applied to attributes deeper in the hierarchy. In case of arrays, a +filter matches if any of the elements of the array matches. In other words, when applying the filter +"(eq,parts/color,green)" to the objects in Example 1, the filter matches obj1 when evaluating the second entry in the +"parts" array of obj1, and matches obj2 already when evaluating the first entry in the "parts" array of obj2. As the result, +both obj1 and obj2 match the filter. + +If a filter contains multiple sub-parts that only differ in the leaf attribute (i.e. they share the same attribute prefix), they +are evaluated together per array entry when traversing an array. As an example, the two expressions in the filter +"(eq,parts/color,green);(eq,parts/id,3)" would be evaluated together for each entry in the array "parts". As the result, +obj2 matches the filter. + +## 6.19.2 Resource definition(s) and HTTP methods + +This pattern is used in GET operations on a resource that will return a list or collection of objects, such as a container +resource with child resources. + +An attribute-based filter shall be represented by a URI query parameter named "filter". The value of this parameter shall +consist of one or more strings formatted according to "simpleFilterExpr", concatenated using the ";" character: + +simpleFilterExprOne := ","["/"]*"," +simpleFilterExprMulti := ","["/"]*","[","]* +simpleFilterExpr := "("")" | "("")" +filterExpr := [";"]* +filter := "filter"= +opOne := "eq" | "neq" | "gt" | "lt" | "gte" | "lte" +opMulti := "in" | "nin" | "cont" | "ncont" +attrName := string +value := string + +where: + +* zero or more occurrences +[] grouping of expressions to be used with * +"" quotation marks for marking string constants +<> name separator +| separator of alternatives + +"AttrName" is the name of one attribute in the data type that defines the representation of the resource. The slash ("/") +character in "simpleFilterExprOne" and " simpleFilterExprMulti" allows concatenation of entries to filter +by attributes deeper in the hierarchy of a structured document. The special attribute name "@key" refers to the key of a +map. + +``` +EXAMPLE 1: Referencing the key of a map +GET .../resource?filter=(eq,mymap/@key,abc123) +``` +The elements "opOne" and "opMulti" stand for the comparison operators (accepting one comparison value or a list of +such values). If the expression has concatenated entries, it means that the operator is applied to the attribute +addressed by the last entry included in the concatenation. All simple filter expressions are combined by the +"AND" logical operator, denoted by ";". + + +In a concatenation of entries in a or , the rightmost + entry is called "leaf attribute". The concatenation of all "attrName" entries except the leaf attribute is called +the "attribute prefix". If an attribute referenced in an expression is an array, an object that contains a corresponding +array shall be considered to match the expression if any of the elements in the array matches all expressions that have +the same attribute prefix. + +The leaf attribute of a or shall not be structured, but shall be of a +simple (scalar) type such as String, Number, Boolean or DateTime, or shall be an array of simple (scalar) values. +Attempting to apply a filter with a structured leaf attribute shall be rejected with "400 Bad Request". A +shall not contain any invalid entry. + +The operators listed in table 6.19.2-1 shall be supported. + +``` +Table 6.19.2-1: Operators for attribute-based filtering +``` +``` +Operator with parameters Meaning +eq,, Attribute equal to +neq, Attribute not equal to +in,,[,]* Attribute equal to one of the values in the list ("in set" relationship) +nin,,[,]* Attribute not equal to any of the values in the list ("not^ in^ set" relationship) +gt,, Attribute greater^ than^ +gte,, Attribute greater than or equal to +lt,, Attribute less than +lte,, Attribute less than or equal to +cont,,[,]* String attribute contains (at least) one of the values in the list +ncont,,[,]* String attribute does not contain any of the values in the list +``` +``` +Table 6.19.2-2: Applicability of the operators to data types +``` +``` +Operator String Number DateTime Enumeration Boolean +eq x x - x x +neq x x - x x +in x x - x - +nin x x - x - +gt x x x - - +gte x x x - - +lt x x x - - +lte x x x - - +cont x - - - - +ncont x - - - - +``` +Table 6.19.2-2 defines which operators are applicable for which data types. All combinations marked with a "x" shall be +supported. + +A entry shall contain a scalar value of type Number, String, Boolean, Enum or DateTime. The content of a + entry shall be formatted the same way as the representation of the related attribute in the resource +representation: + +- The syntax of DateTime entries shall follow the "date-time" production of IETF RFC 3339 [20]. +- The syntax of Boolean and Number entries shall follow IETF RFC 8259 [10]. + +A entry of type String shall be enclosed in single quotes (') if it contains any of the characters ")", "'" or ",", and +may be enclosed in single quotes otherwise. Any single quote (') character contained in a entry shall be +represented as a sequence of two single quote characters. + +The "/" and "~" characters in shall be escaped according to the rules defined in section 3 of IETF +RFC 6901 [4]. If the "," character appears in it shall be escaped by replacing it with "~a". If the "@" +character appears in other than in the keyword "@key", it shall be escaped by replacing it with "~b". + +In the resulting , percent-encoding as defined in IETF RFC 3986 [9] shall be applied to the characters that +are not allowed in a URI query part according to Appendix A of IETF RFC 3986 [9], and to the ampersand "&" +character. + + +``` +NOTE: In addition to the statement on percent-encoding above, it is reminded that the percent "%" character is +always percent-encoded when used in parts of a URI, according to IETF RFC 3986 [9]. +``` +Attribute-based filters are supported for certain resources. Details are defined in the clauses specifying the actual +resources. + +## 6.19.3 Resource representation(s) + +The resource representation in the response body shall only contain those objects that match the filter. + +## 6.19.4 HTTP headers + +There are no specific HTTP headers for this pattern. + +## 6.19.5 Response codes and error handling + +In case of success, the response code 200 OK shall be returned. + +In case of an invalid attribute filtering query, "400 Bad Request" shall be returned, and the response body shall contain a +ProblemDetails structure, in which the "detail" attribute should convey more information about the error. + +## 6.20 Pattern: Handling of too large responses + +## 6.20.1 Description + +If the response to a query to a container resource (i.e. a resource that contains child resources whose representations will +be returned when responding to a GET request) will become so large that the response will adversely affect the +performance of the server, the server either rejects the request with a "400 Bad Request" response, or the server +provides a paged response, i.e. it returns only a subset of the query result in the response, and also provides information +how to obtain the remainder of the query result. + +When returning a paged response, depending on the underlying storage organization, it might be problematic for the +server to determine the actual size of the result; however, it is usually possible to determine whether there will be +additional results returned when knowing, for the last entry in the returned page, the position in the overall query result +or some other property that has ordering semantics. For example, the time of creation of a resource has such an ordering +property. When using such an (implementation-specific) property, the server can correctly handle deletions of child +resources that happen between sending the first page of the query result and sending the next page. It cannot be +guaranteed that child resources inserted between returning subsequent pages can be considered in the query result, +however, it shall be guaranteed that this does not lead to skipping of entries that have existed prior to insertion. + +At minimum, a paged response needs to contain information telling the client that the response is paged, and how to +obtain the next page of information. For that purpose, a link to obtain the next page is returned in an HTTP header, +containing a parameter that is opaque to the client, but that allows the server to determine the start of the next page. + +For each container resource (i.e. a resource that contains child resources whose representations will be returned when +responding to a GET request), the server shall support one of the following two behaviours specified below to handle +the case that a response to a query (GET request) will become so large that the response will adversely affect +performance: + +``` +1) Option 1 (error response): Return an error response if the result set gets too large. +``` +``` +2) Option 2 (paging): Return the result in a paged manner if the result set gets too large. +``` +Clauses 6.20.2, 6.20.3 and 6.20.4 specify these two options. + + +## 6.20.2 Resource definition(s) and HTTP methods + +This pattern is applicable to any container resource and the GET HTTP method. + +For resources that support option 2 (paging), the "nextpage_opaque_marker" URI query parameter shall be supported in +GET requests. + +## 6.20.3 Resource representation(s) + +If option 2 (paging) is supported and the response result is too big to be returned in a single response, the resource +representation in the response body shall contain a single page (subset of the complete query result). + +## 6.20.4 HTTP headers + +If option 2 (paging) is supported, the server shall include in a paged response a LINK HTTP header (see IETF +RFC 8288 [12]) with the "rel" attribute set to "next", which communicates a URI that allows to obtain the next page of +results to the original query, unless there are no further pages available after the current page in which case the LINK +header shall be omitted. + +The client can send a GET request to the URI communicated in the LINK header to obtain the next page of results. The +response which returns that next page shall contain the LINK header to point to the subsequent next page, as specified +above. + +To allow the server to determine the start of the next page, the LINK header shall contain the URI query parameter +"nextpage_opaque_marker" whose value is chosen by the server. This parameter has no meaning for the client but is +echoed back by the client to the server when requesting the next page. The URI in the link header may include further +parameters, such as those passed in the original request. + +The size of each page may be chosen by the API provider and may vary from page to page. The maximum page size is +determined by means outside the scope of the present document. + +The response need not contain entries that correspond to child resources which were created after the original query was +issued. + +## 6.20.5 Response codes and error handling + +If option 1 (error response) is supported, the server shall reject the request with a "400 Bad Request" response, shall +include the "ProblemDetails" payload body, and shall provide in the "detail" attribute more information about the error. + +This error code indicates to the client that with the given attribute-based filtering query (or absence thereof), the +response would have been so big that performance is adversely affected. The client can obtain a query result by +specifying a (more restrictive) attribute-based filtering query (see clause 6.19). + +## 7 Alternative transport mechanisms + +## 7.1 Description + +A MEC service needs a transport to be delivered to a MEC application. The default transport fully specified by ETSI +MEC for MEC service APIs is HTTP-REST. + +An alternative transport can also be specified for certain services that require higher throughput and lower latency than +a REST-based mechanism can provide. Possible alternative transports at the time of writing are topic-based message +buses (e.g. MQTT [i.9] or Apache Kafka® [i.10]) and Remote Procedure Call frameworks (e.g. gRPC® [i.11]). Note that +not all aspects of such alternative transport mechanisms can be fully standardized, but some are left to implementation. + +A transport can either be part of the MEC platform, or can be made available by the MEC application that provides the +service (BYOT - Bring Your Own Transport). REST and gRPC® are always BYOT as the endpoint is the piece of +software that provides the service. + + +Service registration consists of two phases: + +``` +1) Transport discovery (only for non-BYOT) +``` +``` +2) Service registration including transport binding +``` +Step 1 is performed using the "transport discovery" procedure on Mp1 (see ETSI GS MEC 011 [i.6]), to obtain a list of +available transports, and only needed for a non-BYOT service. Step 2 is the "service registration" procedure on Mp1 +which allows to bind a provided service to a transport. This means, a non-BYOT service registers the identifier of the +platform-provided transport it intends to use, and a BYOT service registers the information of its own transport. + +Transport information includes a definition of the access to the transport (e.g. URI, network address, or implementation- +specific), the type of the transport (e.g. HTTP-REST, message bus, RPC, etc.), security information, metadata such as +identifier, name and description, and a container for implementation-specific information. It is further specified in ETSI +GS MEC 011 [i.6]. + +Sending data on a transport requires serialization. There are different serialization formats, for example JSON [10], +XML [11] or Protocol Buffers [i.12]. Binding a service to a transport therefore also requires choosing a serialization +format to be used. The data structures defined for the service can be bound to different serialization formats. The +definition of the binding has to be done as part of the service definition. The JSON binding is typically fully specified +for a RESTful API. Bindings to additional serializers can be provided, either in the documents defined by ETSI MEC, +or in documents provided to the developer community by the MEC application vendors. + +A further aspect of alternative transports is the mechanism how to secure a transport pipe (TLS, typically) and how a +transport or the service that uses it enforces authorization. Enforcing authorization means that the endpoint that provides +the service, or the transport, provides mechanisms to withhold information from unauthorized parties. REST and RPC +transports can work with tokens (e.g. OAuth 2.0, see IETF RFC 6749 [16]) for authorization; here, the service endpoint +is responsible for the enforcement. Message bus transports typically work by using the TLS certificates to enforce +authorization; enforcement can be built into the transport mechanism. In order to realize this in an interoperable way, a +service can define a list of topics to be used with transports that are topic-based, and to use these topics to scope the +access of MEC applications to the actual information. + +## 7.2 Relationship of topics, subscriptions and access rights + +In the RESTful MEC service APIs defined as part of ETSI MEC, a client registers interest in particular changes by +defining a subscription structure that typically contains at least one criterion against which a notification needs to match +in order to be sent to the subscriber for that particular subscription. Multiple criteria can be defined, in which case all +criteria need to match. Each criterion defines a particular value, or a set of values. + +``` +EXAMPLE 1: Table 7.2-1 provides a sample of the criteria part of a data type that represents subscriptions to +notifications about cell changes. +``` +``` +Table 7.2-1: A sample of the criteria part of a data type that represents subscriptions +to notifications about cell changes +``` +``` +Attribute name Data type Cardinality Description +filterCriteria Structure (inlined) 1 List of filtering criteria for the subscription. Any filtering +criteria from below, which is included in the request, shall +also be included in the response. +>appInsId String 0..1 Unique identifier for the MEC application instance. +>associateId array(Structure (inlined)) 0..N +>>type Enum 1 Numeric value (0 - 255) corresponding to specified type of +identifier as following: +0 = reserved +1= UE IPv4 Address +2 = UE IPv6 Address +3 = NATed IP address +4= GTP TEID. +>>value String 1 Value for the identifier. +``` +In topic-based message buses, subscription is done against topics. Each topic is a string that defines the actual event +about which the client wishes to be notified. Typically, topics are organized in a hierarchical structure. Also, in such +structure, often wildcards are allowed that enable to abbreviate the subscription to a complete topic sub-tree. + + +``` +EXAMPLE 2: Criteria from example 1 formulated as topic, prefixed by the service name and notification type +``` +``` +rnis/cell_change/{applnsId}/{associateId.type}/{associateId.value} +``` +``` +EXAMPLE 3: Criteria from example 1 formulated as topic, with wildcard +``` +``` +rnis/cell_change/{applnsId}/* +``` +If a particular MEC service foresees binding to a topic based message bus as an alternative transport, it is encouraged to +define the list or hierarchy of topics in the specification, in order to improve interoperability. If that MEC service also +provides REST-based subscribe-notify, it is encouraged to also define the mapping between the subscription data +structures used in the RESTful API and the topic list/topic hierarchy. + +In MEC, an important feature is authorization of applications. Authorization also needs to apply to subscriptions, to +enable the MEC platform operator to restrict access of MEC applications to privileged information. Each separate +access right is expressed by a "permission identifier" which identifies this right. Permission identifiers need to be +unique within the scope of a particular MEC service. For each access right, the service specification needs to define a +string to name that particular right. These strings can then be used throughout the system to identify that particular +access right. + +For REST-based subscriptions, it is suggested that the set of subscriptions is structured such that the subscription type +can be used to scope the authorization (i.e. clients can be authorized for each individual subscription type separately, +and one permission identifier maps to one subscription type). If finer or coarser granularity is required, this needs to be +expressed in the particular specification by suitably defining the meaning of each permission identifier. For Topic-based +subscriptions, each permission identifier is suggested to map to a particular topic, or a whole sub-tree of the topics +structure. + +The following items are proposed to define permissions: + +``` +Permission identifier: A string that identifies the item to which access is granted or denied. It is unique within the +scope of a particular MEC service specification. +``` +``` +Display name: A short human-readable string to describe the permission when represented towards human +users. +``` +``` +Specification: A specification that defines what the actual permission means. Can be as short as just naming +the resource, subscription type or topic, or can also express a condition to define +authorization at finer granularity than subscription type. If multiple alternative transports are +supported, can contain specifications for more than one transport. +``` +``` +EXAMPLE 4: Tables 7.2-2, 7.2-3 and 7.2-4 provide an example definition of permissions for two transports: +REST-based and topic-based message bus. Queries only apply to the REST-based transport. +``` +``` +Table 7.2-2: Definition of permissions +``` +``` +Permission identifier Display name Remarks +queries Queries REST-based only +bearer_changes Bearer changes Subscribe-notify +priv_bearer_changes Privileged bearer changes Subscribe-notify +``` +``` +Table 7.2-3: Permission identifiers mapping for transport "REST" +``` +``` +Permission identifier Specification +queries Resource: .../rnis/v1/queries +bearer_changes Resource: .../rnis/v1/subscriptions +Subscription type: BearerChangeSubscription with "privileged" flag not set +priv_bearer_changes Resource: .../rnis/v1/subscriptions +Subscription type: BearerChangeSubscription with "privileged" flag set +all All of the permissions identified by "queries", "bearer_changes" and +"priv_bearer_changes". +``` +If OAuth 2.0 is used to authorize access to a REST-based transport, the permission identifiers can be represented as +OAuth 2.0 scope values. + + +``` +Table 7.2-4: Permission identifiers mapping for transport "Topic-based message bus" +``` +``` +Permission identifier Specification +queries Not supported +bearer_changes Topic: /rnis/bearer_changes/nonprivileged/* +priv_bearer_changes Topic: /rnis/bearer_changes/privileged/* +``` +To define the access rights that an application requests, the permission identifiers which represent the requested access +rights are defined in the application descriptor. + +## 7.3 Serializers + +As indicated in clause 7.1, different serializers can be used with alternative transports for a particular service. The +reason for allowing this choice is that certain serializers make more sense in combination with particular transports than +others. For example, RESTful APIs nowadays typically use HTTP/1.1 for transport and JSON for the data payload in +textural form. A large number of development tools support this combination. When using message buses or gRPC®, +typically high throughput and low latency is a main requirement, which can be better met using a serializer into a binary +format. The gRPC framework, for example, uses HTTP/2 for transport and by default Protocol Buffers for binary data +serialization (although other data formats such as JSON in binary form can be used for serialization). Specifications of a +MEC service can define in annexes the serializer(s) that are intended to be used for the data types defined for the +service. The serializer to be used with a transport needs to be signalled over Mp1 when registering a service. More +details can be found in ETSI GS MEC 011 [i.6]. + +## 7.4 Authorization of access to a service over alternative transports using TLS credentials + +A method to authorize access to RESTful MEC service APIs using OAuth 2.0 has been defined in clause 6.16. For +alternative transports, as defined in clause 7, using of OAuth might not be possible or supported in all the cases, e.g. for +topic-based message buses. For these cases, other mechanisms are used to authorize access to the service. Several +alternative transport mechanisms already require using TLS 1.2 [14] or TLS 1.3 [24] to protect the communication +channels. TLS credentials can be used to authenticate the endpoints of the protected connection, and to authorize them +to access the MEC services delivered using an alternative transport that is secured with TLS. + +TLS is designed to provide three essential services: encryption, authentication, and data integrity: + +- For encryption, a secure data channel is established between the peers. To set up this channel, information + about the cipher suite and encryption keys is exchanged between the peers during the TLS handshake. +- As part of the TLS handshake, the procedure also allows the peers to mutually authenticate themselves based + on certificates and chain of trust enabled by Certificate Authorities. In the present document, client access + rights are bound to the TLS credentials related to a client identifier, which allows to authorize an authenticated + client to access particular MEC services or parts of those. +- Besides this, integrity of the data exchanged can be ensured with the Message Authentication Code algorithm + supported by the TLS protocol. + +Figure 7.4-1 shows an example how TLS can be used, in case of a topic-based message bus as alternative transport, to +both secure the communications between the peers, as well as to authorize the consumption of a MEC service by a +MEC application. + +The MEC application is identified by a client identifier, which may be derived from attributes such as Distinguished +Name (DN) used in the client certificate, or the application name and application provider defined in the application +package. In a system that is based on a topic-based message bus as alternative transport, the MEC service is structured +into one or more topics to which the consuming application can subscribe. Permissions can be given to subscribe to +individual topics. By binding these permissions to a client identifier, the MEC application that is identified by this client +identifier can be authorized to consume the corresponding parts of the service. Likewise, a service-producing MEC +application can be authorized to send messages to the message bus for certain topics defined for the service. + + +Figure 7.4-1: Using TLS for authorizing subscription to topics when using +a topic-based message bus + + +As depicted in figure 7.4-1, there are preconditions and related procedures to provision the X.509 certificates for the +message bus and for the MEC application. These procedures are based on the use of a Public Key Infrastructure (PKI) +and they are out of scope of the present document. + +The preconditions for MEC applications assume that authorization-related and security-related parameters are +configured as part of the runtime configuration data of the application, and/or are discovered by the MEC application +over Mp1. These include e.g. TLS version, list of permissions and topics that can be accessed, client identifier such as +Distinguished Name or application provider and application name provided in the application package, and the +instructions how to obtain the client certificate. + +After having obtained the valid certificate to access the specific service offered over the message bus, the MEC +application performs the TLS handshake and subscribes to topics offered, as follows: + +``` +1) The MEC application and the message bus perform the TLS handshake as defined in the TLS protocol +according to the TLS version ([14] for TLS 1.2 or [24] for TLS 1.3), including mutual authentication and +encryption key exchange. As a result, the MEC application is authenticated towards the message bus. +``` +``` +2) The MEC application subscribes to topic N with the message bus. +``` +``` +3) The message bus checks whether the authenticated MEC application is authorized to subscribe to topic N, by +checking the list of authorized topics that were configured for this MEC application. +``` +``` +4) In case the MEC application is not authorized to subscribe to the topic, an "Unauthorized" response is +returned. +``` +Otherwise, in steps 5 through 7 the MEC service sends messages on topics A & B to the message bus. Since the MEC +application has not subscribed to those topics, those messages are not forwarded to this application. + +``` +8) Message on topic N sent to the message bus by the MEC service. +``` +``` +9) The message bus forwards the message to the subscribed MEC application. +``` +In steps 10 and 11 the MEC service sends messages on topics C & G to the message bus. Since the MEC application +has not subscribed to those topics, those messages are not forwarded to this application. + +``` +12) Message on topic N is sent to the message bus by the MEC service. +``` +``` +13) The message bus forwards the message to the subscribed MEC application. +``` + +## Annex A (informative): REST methods................................................................................................ + +``` +All API operations are based on the HTTP Methods. GET and POST are not allowed to be used to tunnel other +operations. +``` +``` +Table A-1 lists basic operations on entities and their mapping to HTTP methods. +``` +Table A-1: Operations and HTTP methods +Operation on entities Uniform API operation Description +Read/Query Entity GET Resource GET is used to retrieve a representation of a +resource. +Create Entity POST Resource POST is used to create a new resource as child +of a collection resource (see note 1). +Create Entity PUT Resource If applicable, PUT can be used to create a new +resource directly (see note 1). +Partial Update of an Entity PATCH Resource PATCH, if supported, is used to partially update +a resource (see note 2). +Complete Update of an Entity PUT Resource PUT is used to completely update a resource +identified by its resource URI. +Remove an Entity DELETE Resource DELETE is used to remove a resource +Execute an Action on an Entity POST on TASK Resource POST on a task resource is used to execute a +specific task not related to +Create/Read/Update/Delete (see note 3). +NOTE 1: It is not advised to mix creation by PUT and creation by POST in the same API. +NOTE 2: PATCH needs to be used with care if it is intended to be idempotent. See [i.2] for general principles. The data +format is defined by IETF RFC 7396 [5]/IETF RFC 6902 [6] for JSON and IETF RFC 5261 [7] for XML. +NOTE 3: A task resource a resource that represents a specific operation that cannot be mapped to a combination of +Create/Read/Update/Delete. Task resources are advised to be used with careful consideration. + + +## Annex B (normative): HTTP response status codes + +The tables in this clause list HTTP response codes typically used in the ETSI MEC REST APIs. In addition to the codes +listed below, clients need to be prepared to receive any other valid HTTP error response code. A list of all valid HTTP +response codes and their specification documents can be obtained from the HTTP status code registry [i.8]. For each +status code, an indicative description and a reference to the related specification are given. The use of each status code +shall follow the provisions in the referenced specification. + +Table B-1 lists the success codes which indicate that the client's request was accepted successfully. + +``` +Table B-1: 2xx - Success codes +Status Code Reference Description +200 IETF RFC 7231 [1] OK - used to indicate nonspecific success. The response body usually +contains a representation of the resource. If this code is returned, it is not +allowed to communicate errors in the response body. +201 IETF RFC 7231 [1] Created - used to indicate successful resource creation. The return message +usually contains a resource representation and always contains a "Location" +HTTP header with the created resource's URI. +202 IETF RFC 7231 [1] Accepted - used to indicate successful start of an asynchronous action. +204 IETF RFC 7231 [1] No Content - used to indicate success when the response body is intentionally +empty. +``` +Table B-2 lists the redirection codes which indicate that the client has to take some additional action in order to +complete its request. + +``` +Table B-2: 3xx - Redirection codes +Status Code Reference Description +301 IETF RFC 7231 [1] Moved Permanently - used to relocate resources. +302 IETF RFC 7231 [1] Found - not used. +303 IETF RFC 7231 [1] See Other - used to refer the client to a different URI. +304 IETF RFC 7231 [1] Not Modified - used to preserve bandwidth. +307 IETF RFC 7231 [1] Temporary Redirect - used to tell clients to resubmit the request to another +URI. +``` +Table B-3 lists the client error codes which indicate an error related to the client's request. Further provisions on the use +of the "ProblemDetails" structure in the payload body of common error responses are defined in annex E. + + +``` +Table B-3: 4xx - Client error codes +Status Code Reference Description +400 IETF RFC 7231 [1] Bad Request - used to indicate a syntactically incorrect request message. +Also used to indicate nonspecific failure caused by the input data, including +"catch-all" errors. +401 IETF RFC 7231 [1] Unauthorized - used when the client did not submit credentials. +403 IETF RFC 7231 [1] Forbidden - used to forbid access regardless of authorization state. +404 IETF RFC 7231 [1] Not Found - used when a client provided a URI that cannot be mapped to a +valid resource URI. +405 IETF RFC 7231 [1] Method Not Allowed - used when the HTTP method is not supported for that +particular resource. Typically, the response includes a list of supported +methods. +406 IETF RFC 7231 [1] Not Acceptable - used to indicate that the server cannot provide the any of +the content formats supported by the client. +409 IETF RFC 7231 [1] Conflict - used when attempting to create a resource that already exists. +410 IETF RFC 7231 [1] Gone - used when a resource is accessed that has existed previously, but +does not exist any longer (if that information is available). +412 IETF RFC 7232 [2] Precondition failed - used when a condition has failed during conditional +requests, e.g. when using ETags to avoid write conflicts when using PUT. +413 IETF RFC 7231 [1] Payload Too Large - The server is refusing to process a request because +the request payload is larger than the server is willing or able to process. +414 IETF RFC 7231 [1] URI Too Long - The server is refusing to process a request because the +request URI is longer than the server is willing or able to process. +415 IETF RFC 7231 [1] Unsupported Media Type - used to indicate that the server or the client does +not support the content type of the payload body. +422 IETF RFC 4918 [21] Unprocessable Entity - used to indicate that the payload body of a request +contains syntactically correct data (e.g. well-formed JSON) but the data +cannot be processed (e.g. because it fails validation against a schema). +429 IETF RFC 6585 [8] Too many requests - used when a rate limiter has triggered. +``` +Table B-4 lists the server error codes which indicate that the server is aware of an error caused at the server side. +Further provisions on the use of the "ProblemDetails" structure in the payload body of common error responses are +defined in annex E. + +``` +Table B-4: 5xx - Server error codes +Status Code Reference Description +500 IETF RFC 7231 [1] Internal Server Error - Server is unable to process the request. Retrying the +same request later might eventually succeed. Also used to indicate +nonspecific failure caused by server processes, including "catch-all" errors. +503 IETF RFC 7231 [1] Service Unavailable - The server is unable to process the request due to +internal overload. Retrying the same request later might eventually succeed. +504 IETF RFC 7231 [1] Gateway Timeout - The server did not receive a timely response from an +upstream server it needed to access in order to complete the request. +Retrying the same request later might eventually succeed. +``` + +## Annex C (informative): Richardson maturity model of REST APIs + +The Richardson maturity model [i.3] breaks down the principal elements of a REST approach into three levels above +the non-REST level 0. + +``` +NOTE: The figure is © by Martin Fowler and has been reproduced with permission from [i.3]. +``` +``` +Figure C-1: Step towards REST +``` +Level 0 - the swamp of POX: it is the starting point, using HTTP as a transport system for remote interactions, but +without using any web mechanisms. Essentially it is to use HTTP as a tunnelling mechanism for remote interaction. + +Level 1 - resources: the first step is to introduce resources. Instead of sending all requests to a singular service endpoint, +they are now addressed to individual resources. + +Level 2 - HTTP methods: HTTP methods (e.g. POST, GET) may be used for interactions in level 0 and 1, but as +tunnelling mechanisms only. Level 2 moves away from this, using the HTTP methods as closely as possible to how they +are used in HTTP itself. + +Level 3 - hypermedia controls: this is often referred to HATEOAS (Hypermedia As The Engine Of Application State). +It addresses the question of how to get from a list of resources to knowing what to do. + +There are several advantages by adopting hypermedia controls: + +- it allows the server to change its URI scheme without breaking clients; +- it helps client developers explore the protocol. + +The links give client developers a hint as to what may be possible next, e.g. a starting point as to what to think about for +more information and to look for a similar URI in the protocol documentation: + +- it allows the server to advertise new capabilities by putting new links in the responses. + +If the client developers are implementing handling for unknown links, these links can be a trigger for further +exploration. + + +## Annex D (informative): RESTful MEC service API template............................................................ + +This annex is a template that provides text blocks for normative specification text to be copied into other specifications. +Therefore, even though this annex is informative, some of the text blocks contain modal verbs that have special +meaning according to clause 3.2 of the ETSI Drafting Rules. A recommendation for the structuring an API definitions +into main clauses is also provided. + +## Sequence diagrams (informative) + +