phase1 opa pdp changes 90/139290/20
authorgururajarao79 <gb00566633@techmahindra.com>
Fri, 22 Nov 2024 13:28:41 +0000 (14:28 +0100)
committergururajarao79 <gb00566633@techmahindra.com>
Wed, 27 Nov 2024 12:26:18 +0000 (13:26 +0100)
For details on scope and implementation, please check.
https://lf-onap.atlassian.net/wiki/spaces/DW/pages/51150925/OPA+PDP
Code Coverage Total: 70.8%

Issue-ID: POLICY-5156
Change-Id: Ied07ee1596e9f447183fb715baaa68c704a9fe99
Signed-off-by: gururajarao79 <gb00566633@techmahindra.com>
90 files changed:
Dockerfile [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.md
api/openapi.yaml [new file with mode: 0644]
api/register-handlers.go [new file with mode: 0644]
api/register-handlers_test.go [new file with mode: 0644]
build/Dockerfile [new file with mode: 0644]
build/Makefile [new file with mode: 0644]
build/build_image.sh [new file with mode: 0755]
cfg/config.go [new file with mode: 0644]
cfg/config_test.go [new file with mode: 0644]
cmd/opa-pdp/opa-pdp.go [new file with mode: 0644]
cmd/opa-pdp/opa-pdp_test.go [new file with mode: 0644]
consts/constants.go [new file with mode: 0644]
go.mod [new file with mode: 0644]
go.sum [new file with mode: 0644]
pkg/bundleserver/bundle-server.go [new file with mode: 0644]
pkg/bundleserver/bundle-server_test.go [new file with mode: 0644]
pkg/decision/decision-provider.go [new file with mode: 0644]
pkg/decision/decision-provider_test.go [new file with mode: 0644]
pkg/healthcheck/healthcheck.go [new file with mode: 0644]
pkg/healthcheck/healthcheck_test.go [new file with mode: 0644]
pkg/kafkacomm/handler/pdp_message_handler.go [new file with mode: 0644]
pkg/kafkacomm/handler/pdp_message_handler_test.go [new file with mode: 0644]
pkg/kafkacomm/handler/pdp_state_change_handler.go [new file with mode: 0644]
pkg/kafkacomm/handler/pdp_state_change_handler_test.go [new file with mode: 0644]
pkg/kafkacomm/handler/pdp_update_message_handler.go [new file with mode: 0644]
pkg/kafkacomm/handler/pdp_update_message_handler_test.go [new file with mode: 0644]
pkg/kafkacomm/mocks/kafkaconsumerinterface.go [new file with mode: 0644]
pkg/kafkacomm/mocks/kafkaproducerinterface.go [new file with mode: 0644]
pkg/kafkacomm/pdp_topic_consumer.go [new file with mode: 0644]
pkg/kafkacomm/pdp_topic_consumer_test.go [new file with mode: 0644]
pkg/kafkacomm/pdp_topic_producer.go [new file with mode: 0644]
pkg/kafkacomm/pdp_topic_producer_test.go [new file with mode: 0644]
pkg/kafkacomm/publisher/mocks/PdpStatusSender.go [new file with mode: 0644]
pkg/kafkacomm/publisher/pdp-heartbeat.go [new file with mode: 0644]
pkg/kafkacomm/publisher/pdp-heartbeat_test.go [new file with mode: 0644]
pkg/kafkacomm/publisher/pdp-pap-registration.go [new file with mode: 0644]
pkg/kafkacomm/publisher/pdp-pap-registration_test.go [new file with mode: 0644]
pkg/kafkacomm/publisher/pdp-status-publisher.go [new file with mode: 0644]
pkg/kafkacomm/publisher/pdp-status-publisher_test.go [new file with mode: 0644]
pkg/log/log.go [new file with mode: 0644]
pkg/log/log_test.go [new file with mode: 0644]
pkg/metrics/counters.go [new file with mode: 0644]
pkg/metrics/counters_test.go [new file with mode: 0644]
pkg/metrics/statistics-provider.go [new file with mode: 0644]
pkg/metrics/statistics-provider_test.go [new file with mode: 0644]
pkg/model/healthcheckmessage.go [new file with mode: 0644]
pkg/model/mesages.go [new file with mode: 0644]
pkg/model/messages_test.go [new file with mode: 0644]
pkg/model/oapicodegen/models.go [new file with mode: 0644]
pkg/model/pdphealthstatus.go [new file with mode: 0644]
pkg/model/pdphealthstatus_test.go [new file with mode: 0644]
pkg/model/pdpresponsedetails.go [new file with mode: 0644]
pkg/model/pdpresponsedetails_test.go [new file with mode: 0644]
pkg/model/pdpstate.go [new file with mode: 0644]
pkg/model/pdpstate_test.go [new file with mode: 0644]
pkg/model/toscaconceptidentifier.go [new file with mode: 0644]
pkg/model/toscaconceptidentifier_test.go [new file with mode: 0644]
pkg/opasdk/opasdk.go [new file with mode: 0644]
pkg/opasdk/opasdk_test.go [new file with mode: 0644]
pkg/pdpattributes/pdpattributes.go [new file with mode: 0644]
pkg/pdpattributes/pdpattributes_test.go [new file with mode: 0644]
pkg/pdpstate/pdpstate.go [new file with mode: 0644]
pkg/pdpstate/pdpstate_test.go [new file with mode: 0644]
pkg/utils/utils.go [new file with mode: 0644]
pkg/utils/utils_test.go [new file with mode: 0644]
test/Opagroup.json [new file with mode: 0644]
test/README.md [new file with mode: 0644]
test/config.json [new file with mode: 0644]
test/config/opa-pdp/config.json [new file with mode: 0644]
test/config/opa-pdp/groups.json [new file with mode: 0644]
test/config/opa-pdp/policy-opa-pdp.sh [new file with mode: 0755]
test/docker-compose.yml [new file with mode: 0644]
test/policies/abac/policy.rego [new file with mode: 0644]
test/policies/account/policy.rego [new file with mode: 0644]
test/policies/action/policy.rego [new file with mode: 0644]
test/policies/data/abac/data.json [new file with mode: 0644]
test/policies/data/account/data.json [new file with mode: 0644]
test/policies/data/action/data.json [new file with mode: 0644]
test/policies/data/organization/data.json [new file with mode: 0644]
test/policies/data/role/data.json [new file with mode: 0644]
test/policies/example/policy.rego [new file with mode: 0644]
test/policies/organization/policy.rego [new file with mode: 0644]
test/policies/role/policy.rego [new file with mode: 0644]
test/policy-new.yaml [new file with mode: 0644]
test/scripts.sh [new file with mode: 0755]
test/scripts.txt [new file with mode: 0644]
test/wait_for_port.sh [new file with mode: 0644]
version [new file with mode: 0644]

diff --git a/Dockerfile b/Dockerfile
new file mode 100644 (file)
index 0000000..389c328
--- /dev/null
@@ -0,0 +1,58 @@
+FROM curlimages/curl:7.78.0 AS build
+
+# Get OPA
+RUN curl -Lo /tmp/opa https://github.com/open-policy-agent/opa/releases/download/v0.69.0/opa_linux_amd64
+
+FROM golang:1.23 AS compile
+
+RUN mkdir /app
+
+COPY go.mod go.sum /app/
+
+COPY . .
+
+RUN mkdir /app/cfg
+ADD cfg /app/cfg
+
+RUN mkdir /app/consts
+ADD consts /app/consts
+
+RUN mkdir /app/api
+ADD api /app/api
+
+RUN mkdir /app/cmd
+ADD cmd /app/cmd
+
+RUN mkdir /app/pkg
+ADD pkg /app/pkg
+
+RUN mkdir /app/bundles
+
+WORKDIR /app
+
+# Build the binary
+RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /app/opa-pdp /app/cmd/opa-pdp/opa-pdp.go
+#COPY config.json /app/config.json
+#RUN chmod 644 /app/config.json
+
+FROM ubuntu
+
+RUN apt-get update && apt-get install -y netcat-openbsd && rm -rf /var/lib/apt/lists/*
+
+RUN apt-get update && apt-get install -y curl
+
+# Copy our static executable from compile stage
+RUN mkdir /app
+COPY --from=compile /app /app
+RUN chmod +x /app/opa-pdp
+
+# Copy our opa executable from build stage
+COPY --from=build /tmp/opa /app/opa
+RUN chmod 755 /app/opa
+
+WORKDIR /app
+EXPOSE 8282
+
+# Command to run OPA with the policies
+CMD ["/app/opa-pdp"]
+
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..7883b7f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,31 @@
+PWD := $(shell pwd)
+PLATFORM := linux
+BINARY := opa-pdp
+
+
+all: test build
+deploy: test build
+
+build: build_image
+
+deploy: build
+
+.PHONY: test
+test: clean
+       @go test -v ./...
+
+format:
+       @go fmt ./...
+
+clean:
+       @rm -f $(BINARY)
+
+.PHONY: cover
+cover:
+       @go test -p 2 ./... -coverprofile=coverage.out
+       @go tool cover -html=coverage.out -o coverage.html
+
+build_image:
+       docker build -f  Dockerfile  -t policy-opa-pdp:1.0.0 .
+       docker tag policy-opa-pdp:1.0.0 nexus3.onap.org:10003/onap/policy-opa-pdp:latest
+       docker tag nexus3.onap.org:10003/onap/policy-opa-pdp:latest nexus3.onap.org:10003/onap/policy-opa-pdp:1.0.0
index 81636e5..e7603f2 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # Running docker  policy-opa-pdp
 
 ## Building Docker Image.
-docker build -f  ./build/Dockerfile  -t opa-pdp:1.1.1 .
+docker build -f  ./build/Dockerfile  -t opa-pdp:1.0.0 .
 
 ## Running the containers and Testing
 
@@ -13,4 +13,68 @@ docker build -f  ./build/Dockerfile  -t opa-pdp:1.1.1 .
 
 4.  docker logs -f opa-pdp
 
+## Generating models with openapi.yaml
+   
+1. oapi-codegen -package=oapicodegen  -generate "models" openapi.yaml > models.go
+
+## Creating new Policy
+
+1. Create a new directory under test/polices. For example - role
+
+2. Inside this directory create a policy [i.e; rego file] named policy.rego. Version 1 i.e v1 is supported  for rego files.
+
+3. For contents you can see example of  policy.rego under test/policies/role/policy.rego. 
+
+3. Inside test/policies/data create a new directory with the package name of policy.rego. For example test/policies/data/role
+
+4. Create a file data.json under the newly created directory inside data. For example test/policies/data/data.json
+
+5. In policy.rego the package declaration organizes the policy rules. This allows 
+
+6. The Rule allow evaluates to true/false based on the logic defined in policy.rego
+
+7. Data.json is files is kept within the directory named after policy package name under data folders. For example policies/data/role/data.json.
+
+8. To reference the data inside policy.rego we need to define rule as data.folder-name.attribute. For example you can refer to policy.rego under rules, data.role.user_roles.
+
+9. To deploy a new policy opa-pdp need to be redpolyed i.e; docker-compose down and up need to be executed.
+
+## Testing Decision Api
+
+send json 
+{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS","currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC",  "timeOffset": "+05:30", "currentDateTime": "2024-11-22 12:08:00.123456+0000 ", "policyName":"role/allow","input":{"user":"alice","action":"write","object":"id123","type":"dog"}} 
+to opa-pdp as shown in curl commands below.
+
+"policyName":"[packagename in rego file]/allow"
+  Policy to be refrenced as policyName:role/allow in case when policy's package name is role. Change it according to  your package name of the policy.
+
+"input":{"user":"alice","action":"read","object":"id123","type":"dog"}
+  Input defines the specific data to be evaluated by the Rego policy
+
+## Verification API Calls
+
+`curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS","currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC", "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z", "policyName":"role/allow","input":{"user":"alice","action":"write","object":"id123","type":"dog"}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision`
+
+## Result Of Verification API calls(Success)
+
+`curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS","currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC",  "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z", "policyName":"role/allow","input":{"user":"alice","action":"write","object":"id123","type":"dog"}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision`
+
+`{"decision":"PERMIT","policyName":"role/allow","statusMessage":"OPA Allowed"}`
+
+
+## Result of Verification API calls(Failure)
+
+`curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS","currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC",  "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z", "policyName":"role/allow","input":{"user":"carol","action":"write","object":"id123","type":"dog"}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision`
+
+## HealthCheck API Call With Response
+
+`curl -u 'policyadmin:zb!XztG34' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -X GET http://0.0.0.0:8282/policy/pdpx/v1/healthcheck`
+
+`{"code":200,"healthy":true,"message":"alive","name":"opa-9f0248ea-807e-45f6-8e0f-935e570b75cc","url":"self"}`
+
+## Statistics API Call With Response
+
+`curl -u 'policyadmin:zb!XztG34' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -X GET http://0.0.0.0:8282/policy/pdpx/v1/statistics`
+
+`{"code":200,"denyDecisionsCount":10,"deployFailureCount":0,"deploySuccessCount":0,"indeterminantDecisionsCount":0,"permitDecisionsCount":18,"totalErrorCount":4,"totalPoliciesCount":0,"totalPolicyTypesCount":1,"undeployFailureCount":0,"undeploySuccessCount":0}`
 
diff --git a/api/openapi.yaml b/api/openapi.yaml
new file mode 100644 (file)
index 0000000..aaff2b9
--- /dev/null
@@ -0,0 +1,374 @@
+#\r
+#  ========================LICENSE_START=================================\r
+#   Copyright (C) 2024: Deutsche Telecom\r
+#\r
+#   Licensed under the Apache License, Version 2.0 (the "License");\r
+#   you may not use this file except in compliance with the License.\r
+#   You may obtain a copy of the License at\r
+#\r
+#        http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+#   Unless required by applicable law or agreed to in writing, software\r
+#   distributed under the License is distributed on an "AS IS" BASIS,\r
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+#   See the License for the specific language governing permissions and\r
+#  limitations under the License.\r
+#  ========================LICENSE_END===================================\r
+#\r
+openapi: 3.0.3\r
+info:\r
+  title: "Policy OPA PDP Documentation"\r
+  description: Policy OPA PDP Service\r
+  version: 1.0.0\r
+  x-component: Policy Framework\r
+  x-planned-retirement-date: tbd\r
+  contact:\r
+      name: Deena Mukundan\r
+      email: dm00536893@techmahindra.com\r
+servers:\r
+- url: http://policy-opa-pdp:8282/policy/pdpx/v1\r
+- url: https://policy-opa-pdp:8282/policy/pdpx/v1\r
+tags:\r
+- name: Decision\r
+- name: Statistics\r
+- name: HealthCheck\r
+paths:\r
+  /decision:\r
+    post:\r
+      tags:\r
+      - Decision\r
+      summary: Fetch the decision using specified decision parameters\r
+      description: Returns the policy decision from Policy OPA PDP\r
+      operationId: decision\r
+      parameters:\r
+      - name: X-ONAP-RequestID\r
+        in: header\r
+        description: RequestID for http transaction\r
+        schema:\r
+          type: string\r
+          format: uuid\r
+      requestBody:\r
+        content:\r
+          application/json:\r
+            schema:\r
+              $ref: '#/components/schemas/OPADecisionRequest'\r
+          application/yaml:\r
+            schema:\r
+              $ref: '#/components/schemas/OPADecisionRequest'\r
+        required: false\r
+      responses:\r
+        200:\r
+          description: successful operation\r
+          headers:\r
+            X-LatestVersion:\r
+              description: Used only to communicate an API's latest version\r
+              schema:\r
+                type: string\r
+            X-PatchVersion:\r
+              description: Used only to communicate a PATCH version in a response\r
+                for troubleshooting purposes only, and will not be provided by the\r
+                client on request\r
+              schema:\r
+                type: string\r
+            X-MinorVersion:\r
+              description: Used to request or communicate a MINOR version back from\r
+                the client to the server, and from the server back to the client\r
+              schema:\r
+                type: string\r
+            X-ONAP-RequestID:\r
+              description: Used to track REST transactions for logging purpose\r
+              schema:\r
+                type: string\r
+                format: uuid\r
+          content:\r
+            application/json:\r
+              schema:\r
+                $ref: '#/components/schemas/OPADecisionResponse'\r
+            application/yaml:\r
+              schema:\r
+                $ref: '#/components/schemas/OPADecisionResponse'\r
+        400:\r
+          description: Bad Request\r
+          content:\r
+            application/json:\r
+              schema:\r
+                $ref: '#/components/schemas/ErrorResponse'\r
+            application/yaml:\r
+              schema:\r
+                $ref: '#/components/schemas/ErrorResponse'\r
+        401:\r
+          description: Authentication Error\r
+          content: {}\r
+        403:\r
+          description: Authorization Error\r
+          content: {}\r
+        500:\r
+          description: Internal Server Error\r
+          content: {}\r
+      security:\r
+      - basicAuth: []\r
+      x-interface info:\r
+        last-mod-release: Paris\r
+        pdpx-version: 1.0.0\r
+      x-codegen-request-body-name: body\r
+  /healthcheck:\r
+    get:\r
+      tags:\r
+      - HealthCheck\r
+      summary: Perform a system healthcheck\r
+      description: Provides healthy status of the Policy OPA PDP component\r
+      operationId: healthcheck\r
+      parameters:\r
+      - name: X-ONAP-RequestID\r
+        in: header\r
+        description: RequestID for http transaction\r
+        schema:\r
+          type: string\r
+          format: uuid\r
+      responses:\r
+        200:\r
+          description: successful operation\r
+          headers:\r
+            X-LatestVersion:\r
+              description: Used only to communicate an API's latest version\r
+              schema:\r
+                type: string\r
+            X-PatchVersion:\r
+              description: Used only to communicate a PATCH version in a response\r
+                for troubleshooting purposes only, and will not be provided by the\r
+                client on request\r
+              schema:\r
+                type: string\r
+            X-MinorVersion:\r
+              description: Used to request or communicate a MINOR version back from\r
+                the client to the server, and from the server back to the client\r
+              schema:\r
+                type: string\r
+            X-ONAP-RequestID:\r
+              description: Used to track REST transactions for logging purpose\r
+              schema:\r
+                type: string\r
+                format: uuid\r
+          content:\r
+            application/json:\r
+              schema:\r
+                $ref: '#/components/schemas/HealthCheckReport'\r
+            application/yaml:\r
+              schema:\r
+                $ref: '#/components/schemas/HealthCheckReport'\r
+        401:\r
+          description: Authentication Error\r
+          content: {}\r
+        403:\r
+          description: Authorization Error\r
+          content: {}\r
+        500:\r
+          description: Internal Server Error\r
+          content: {}\r
+      security:\r
+      - basicAuth: []\r
+      x-interface info:\r
+        last-mod-release: Paris\r
+        pdpx-version: 1.0.0\r
+  /statistics:\r
+    get:\r
+      tags:\r
+      - Statistics\r
+      summary: Fetch current statistics\r
+      description: Provides current statistics of the Policy OPA PDP component \r
+      operationId: statistics\r
+      parameters:\r
+      - name: X-ONAP-RequestID\r
+        in: header\r
+        description: RequestID for http transaction\r
+        schema:\r
+          type: string\r
+          format: uuid\r
+      responses:\r
+        200:\r
+          description: successful operation\r
+          headers:\r
+            X-LatestVersion:\r
+              description: Used only to communicate an API's latest version\r
+              schema:\r
+                type: string\r
+            X-PatchVersion:\r
+              description: Used only to communicate a PATCH version in a response\r
+                for troubleshooting purposes only, and will not be provided by the\r
+                client on request\r
+              schema:\r
+                type: string\r
+            X-MinorVersion:\r
+              description: Used to request or communicate a MINOR version back from\r
+                the client to the server, and from the server back to the client\r
+              schema:\r
+                type: string\r
+            X-ONAP-RequestID:\r
+              description: Used to track REST transactions for logging purpose\r
+              schema:\r
+                type: string\r
+                format: uuid\r
+          content:\r
+            application/json:\r
+              schema:\r
+                $ref: '#/components/schemas/StatisticsReport'\r
+            application/yaml:\r
+              schema:\r
+                $ref: '#/components/schemas/StatisticsReport'\r
+        401:\r
+          description: Authentication Error\r
+          content: {}\r
+        403:\r
+          description: Authorization Error\r
+          content: {}\r
+        500:\r
+          description: Internal Server Error\r
+          content: {}\r
+      security:\r
+      - basicAuth: []\r
+      x-interface info:\r
+        last-mod-release: Paris\r
+        pdpx-version: 1.0.0\r
+components:\r
+  schemas:\r
+    ErrorResponse:\r
+      type: object\r
+      properties:\r
+        responseCode:\r
+          type: string\r
+          enum:\r
+          - BAD_REQUEST\r
+          - UNAUTHORIZED\r
+          - METHOD_NOT_ALLOWED\r
+          - NOT_ACCEPTABLE\r
+          - REQUEST_TIMEOUT\r
+          - CONFLICT\r
+          - GONE\r
+          - LENGTH_REQUIRED\r
+          - PRECONDITION_FAILED\r
+          - REQUEST_ENTITY_TOO_LARGE\r
+          - REQUEST_URI_TOO_LONG\r
+          - UNSUPPORTED_MEDIA_TYPE\r
+          - REQUESTED_RANGE_NOT_SATISFIABLE\r
+          - EXPECTATION_FAILED\r
+          - PRECONDITION_REQUIRED\r
+          - TOO_MANY_REQUESTS\r
+          - REQUEST_HEADER_FIELDS_TOO_LARGE\r
+          - INTERNAL_SERVER_ERROR\r
+          - NOT_IMPLEMENTED\r
+          - BAD_GATEWAY\r
+          - SERVICE_UNAVAILABLE\r
+          - GATEWAY_TIMEOUT\r
+          - HTTP_VERSION_NOT_SUPPORTED\r
+          - NETWORK_AUTHENTICATION_REQUIRED\r
+        errorMessage:\r
+          type: string\r
+        policyName:\r
+          type: string\r
+        errorDetails:\r
+          type: array\r
+          items:\r
+            type: string\r
+    OPADecisionRequest:\r
+      type: object\r
+      properties:\r
+        onapName:\r
+          type: string\r
+        onapComponent:\r
+          type: string\r
+        onapInstance:\r
+          type: string\r
+        currentDateTime:\r
+          type: string\r
+          format: date-time\r
+        currentDate:\r
+          type: string\r
+          format: date\r
+        currentTime:\r
+          type: string\r
+          format: date-time\r
+        timeZone:\r
+          type: string\r
+          description: "Timezone in IANA format (e.g., 'America/NewYork', 'Europe/Paris', 'UTC')"\r
+        timeOffset:\r
+          type: string\r
+          pattern: '^[+-]?\d{2}:\d{2}$'\r
+          description: "Time offset in hours and minutes, e.g., '+02:00' or '-05:00'"\r
+        policyName:\r
+          type: string\r
+        input:\r
+          type: object\r
+          additionalProperties: true\r
+          example:\r
+                    user: alice\r
+                    action: read\r
+                    object: id123\r
+                    type: dog\r
+    HealthCheckReport:\r
+      type: object\r
+      properties:\r
+        name:\r
+          type: string\r
+        url:\r
+          type: string\r
+        healthy:\r
+          type: boolean\r
+        code:\r
+          type: integer\r
+          format: int32\r
+        message:\r
+          type: string\r
+    OPADecisionResponse:\r
+      type: object\r
+      properties:\r
+        statusMessage:\r
+          type: string\r
+        decision:\r
+          type: string\r
+          enum:\r
+          - PERMIT\r
+          - DENY\r
+          - INDETERMINATE\r
+        policyName:\r
+          type: string\r
+    StatisticsReport:\r
+      type: object\r
+      properties:\r
+        code:\r
+          type: integer\r
+          format: int32\r
+        totalPolicyTypesCount:\r
+          type: integer\r
+          format: int64\r
+        totalPoliciesCount:\r
+          type: integer\r
+          format: int64\r
+        totalErrorCount:\r
+          type: integer\r
+          format: int64\r
+        permitDecisionsCount:\r
+          type: integer\r
+          format: int64\r
+        denyDecisionsCount:\r
+          type: integer\r
+          format: int64\r
+        deploySuccessCount:\r
+          type: integer\r
+          format: int64\r
+        deployFailureCount:\r
+          type: integer\r
+          format: int64\r
+        undeploySuccessCount:\r
+          type: integer\r
+          format: int64\r
+        undeployFailureCount:\r
+          type: integer\r
+          format: int64\r
+        indeterminantDecisionsCount:\r
+          type: integer\r
+          format: int64\r
+  securitySchemes:\r
+    basicAuth:\r
+      type: http\r
+      description: ""\r
+      scheme: basic
\ No newline at end of file
diff --git a/api/register-handlers.go b/api/register-handlers.go
new file mode 100644 (file)
index 0000000..37028d2
--- /dev/null
@@ -0,0 +1,81 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package api provides HTTP handlers for the policy-opa-pdp service.
+// This package includes handlers for decision making, bundle serving, health checks, and readiness probes.
+// It also includes basic authentication middleware for securing certain endpoints.
+package api
+
+import (
+       "net/http"
+       "policy-opa-pdp/cfg"
+       "policy-opa-pdp/pkg/bundleserver"
+       "policy-opa-pdp/pkg/decision"
+       "policy-opa-pdp/pkg/healthcheck"
+       "policy-opa-pdp/pkg/metrics"
+)
+
+// RegisterHandlers registers the HTTP handlers for the service.
+func RegisterHandlers() {
+
+       // Handler for OPA decision making
+       opaDecisionHandler := http.HandlerFunc(decision.OpaDecision)
+       http.Handle("/policy/pdpx/v1/decision", basicAuth(opaDecisionHandler))
+
+       //This api is used internally by OPA-SDK
+       bundleServerHandler := http.HandlerFunc(bundleserver.GetBundle)
+       http.Handle("/opa/bundles/", bundleServerHandler)
+
+       // Handler for kubernetes readiness probe
+       readinessProbeHandler := http.HandlerFunc(readinessProbe)
+       http.Handle("/ready", readinessProbeHandler)
+
+       // Handler for health checks
+       healthCheckHandler := http.HandlerFunc(healthcheck.HealthCheckHandler)
+       http.HandleFunc("/policy/pdpx/v1/healthcheck", basicAuth(healthCheckHandler))
+
+       // Handler for statistics report
+       statisticsReportHandler := http.HandlerFunc(metrics.FetchCurrentStatistics)
+       http.HandleFunc("/policy/pdpx/v1/statistics", basicAuth(statisticsReportHandler))
+
+}
+
+// handles authentication
+func basicAuth(next http.HandlerFunc) http.HandlerFunc {
+       return func(res http.ResponseWriter, req *http.Request) {
+               user, pass, ok := req.BasicAuth()
+               if !ok || !validateCredentials(user, pass) {
+                       res.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
+                       http.Error(res, "Unauthorized", http.StatusUnauthorized)
+                       return
+               }
+               next(res, req)
+       }
+}
+
+// validates Credentials for http server
+func validateCredentials(username, password string) bool {
+       validUser := cfg.Username
+       validPass := cfg.Password
+       return username == validUser && password == validPass
+}
+
+// handles readiness probe endpoint
+func readinessProbe(res http.ResponseWriter, req *http.Request) {
+       res.WriteHeader(http.StatusOK)
+       res.Write([]byte("Ready"))
+}
diff --git a/api/register-handlers_test.go b/api/register-handlers_test.go
new file mode 100644 (file)
index 0000000..72624f8
--- /dev/null
@@ -0,0 +1,115 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package api
+
+import (
+       "net/http"
+       "net/http/httptest"
+       "policy-opa-pdp/cfg"
+       "policy-opa-pdp/pkg/bundleserver"
+       "policy-opa-pdp/pkg/decision"
+       "policy-opa-pdp/pkg/healthcheck"
+       "testing"
+)
+
+// Mock configuration
+func init() {
+       cfg.Username = "testuser"
+       cfg.Password = "testpass"
+}
+
+func TestRegisterHandlers(t *testing.T) {
+       RegisterHandlers()
+
+       tests := []struct {
+               path       string
+               handler    http.HandlerFunc
+               statusCode int
+       }{
+               {"/policy/pdpx/v1/decision", decision.OpaDecision, http.StatusUnauthorized},
+               {"/opa/bundles/", bundleserver.GetBundle, http.StatusInternalServerError},
+               {"/ready", readinessProbe, http.StatusOK},
+               {"/policy/pdpx/v1/healthcheck", healthcheck.HealthCheckHandler, http.StatusUnauthorized},
+       }
+
+       for _, tt := range tests {
+               req, err := http.NewRequest("GET", tt.path, nil)
+               if err != nil {
+                       t.Fatalf("Failed to create request: %v", err)
+               }
+
+               rr := httptest.NewRecorder()
+               http.DefaultServeMux.ServeHTTP(rr, req)
+
+               if status := rr.Code; status != tt.statusCode {
+                       t.Errorf("handler for %s returned wrong status code: got %v want %v", tt.path, status, tt.statusCode)
+               }
+       }
+}
+
+func TestBasicAuth(t *testing.T) {
+       handler := basicAuth(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+               res.WriteHeader(http.StatusOK)
+       }))
+
+       tests := []struct {
+               username   string
+               password   string
+               statusCode int
+       }{
+               {"testuser", "testpass", http.StatusOK},
+               {"wronguser", "wrongpass", http.StatusUnauthorized},
+               {"", "", http.StatusUnauthorized},
+       }
+
+       for _, tt := range tests {
+               req, err := http.NewRequest("GET", "/", nil)
+               if err != nil {
+                       t.Fatalf("Failed to create request: %v", err)
+               }
+               req.SetBasicAuth(tt.username, tt.password)
+
+               rr := httptest.NewRecorder()
+               handler.ServeHTTP(rr, req)
+
+               if status := rr.Code; status != tt.statusCode {
+                       t.Errorf("basicAuth returned wrong status code: got %v want %v", status, tt.statusCode)
+               }
+       }
+}
+
+func TestReadinessProbe(t *testing.T) {
+       req, err := http.NewRequest("GET", "/ready", nil)
+       if err != nil {
+               t.Fatalf("Failed to create request: %v", err)
+       }
+
+       rr := httptest.NewRecorder()
+       handler := http.HandlerFunc(readinessProbe)
+       handler.ServeHTTP(rr, req)
+
+       if status := rr.Code; status != http.StatusOK {
+               t.Errorf("readinessProbe returned wrong status code: got %v want %v", status, http.StatusOK)
+       }
+
+       expected := "Ready"
+       if rr.Body.String() != expected {
+               t.Errorf("readinessProbe returned unexpected body: got %v want %v", rr.Body.String(), expected)
+       }
+}
diff --git a/build/Dockerfile b/build/Dockerfile
new file mode 100644 (file)
index 0000000..2905d77
--- /dev/null
@@ -0,0 +1,58 @@
+FROM curlimages/curl:7.78.0 AS build
+
+# Get OPA
+RUN curl -Lo /tmp/opa https://github.com/open-policy-agent/opa/releases/download/v0.69.0/opa_linux_amd64
+
+FROM golang:1.23 AS compile
+
+RUN mkdir /app
+
+COPY ../go.mod ../go.sum /app/
+
+COPY . .
+
+RUN mkdir /app/cfg
+ADD ../cfg /app/cfg
+
+RUN mkdir /app/consts
+ADD ../consts /app/consts
+
+RUN mkdir /app/api
+ADD ../api /app/api
+
+RUN mkdir /app/cmd
+ADD ../cmd /app/cmd
+
+RUN mkdir /app/pkg
+ADD ../pkg /app/pkg
+
+RUN mkdir /app/bundles
+
+WORKDIR /app
+
+# Build the binary
+RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /app/opa-pdp /app/cmd/opa-pdp/opa-pdp.go
+#COPY config.json /app/config.json
+#RUN chmod 644 /app/config.json
+
+FROM ubuntu
+
+RUN apt-get update && apt-get install -y netcat-openbsd && rm -rf /var/lib/apt/lists/*
+
+RUN apt-get update && apt-get install -y curl
+
+# Copy our static executable from compile stage
+RUN mkdir /app
+COPY --from=compile /app /app
+RUN chmod +x /app/opa-pdp
+
+# Copy our opa executable from build stage
+COPY --from=build /tmp/opa /app/opa
+RUN chmod 755 /app/opa
+
+WORKDIR /app
+EXPOSE 8282
+
+# Command to run OPA with the policies
+CMD ["/app/opa-pdp"]
+
diff --git a/build/Makefile b/build/Makefile
new file mode 100644 (file)
index 0000000..d459215
--- /dev/null
@@ -0,0 +1,4 @@
+SHELL := /bin/bash
+
+build:
+       ./build_image.sh
diff --git a/build/build_image.sh b/build/build_image.sh
new file mode 100755 (executable)
index 0000000..9b44a47
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/bash
+# -
+#   ========================LICENSE_START=================================
+#   Copyright (C) 2024: Deutsche Telecom
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+#   ========================LICENSE_END===================================
+#
+
+export IMAGE_NAME="nexus3.onap.org:10003/onap/policy-opa-pdp"
+VERSION_FILE="../version"
+
+
+# Check for the version file
+# If it exists, load the version from that file
+# If not found, then use the current version as 1.1.0 for docker images
+if [ -f "$VERSION_FILE" ]; then
+    VERSION=`cat ../version|xargs echo`;
+else
+    VERSION=1.0.0;
+fi
+
+
+function  _build_docker_and_push_image {
+    local tag_name=${IMAGE_NAME}:${VERSION}
+    
+    docker build -f  Dockerfile  -t policy-opa-pdp:${VERSION} ../.
+    echo "Start push {$tag_name}"
+    docker tag policy-opa-pdp:${VERSION} ${IMAGE_NAME}:latest
+    docker push ${IMAGE_NAME}:latest
+    docker tag ${IMAGE_NAME}:latest ${tag_name}
+    docker push ${tag_name}
+}
+
+_build_docker_and_push_image
diff --git a/cfg/config.go b/cfg/config.go
new file mode 100644 (file)
index 0000000..4840688
--- /dev/null
@@ -0,0 +1,103 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package cfg provides configuration settings for the policy-opa-pdp service.
+// This package includes variables for various configuration settings such as log level,
+// Kafka server details, and credentials.It also includes functions to initialize these
+// settings and retrieve environment variables with default values.
+package cfg
+
+import (
+       log "github.com/sirupsen/logrus"
+       "os"
+       "strconv"
+)
+
+// LogLevel        - The log level for the application.
+// BootstrapServer - The Kafka bootstrap server address.
+// Topic           - The Kafka topic to subscribe to.
+// GroupId         - The Kafka consumer group ID.
+// Username        - The username for basic authentication.
+// Password        - The password for basic authentication.
+// UseSASLForKAFKA - Flag to indicate if SASL should be used for Kafka.
+// KAFKA_USERNAME  - The Kafka username for SASL authentication.
+// KAFKA_PASSWORD  - The Kafka password for SASL authentication.
+var (
+       LogLevel        string
+       BootstrapServer string
+       Topic           string
+       GroupId         string
+       Username        string
+       Password        string
+       UseSASLForKAFKA string
+       KAFKA_USERNAME  string
+       KAFKA_PASSWORD  string
+)
+
+// Initializes the configuration settings.
+func init() {
+
+       log.SetLevel(log.DebugLevel)
+       log.SetOutput(os.Stdout)
+
+       log.Debug("###################################### ")
+       log.Debug("OPA-PDP: Starting initialisation ")
+       log.Debug("###################################### ")
+
+       LogLevel = getEnv("LOG_LEVEL", "info")
+       BootstrapServer = getEnv("KAFKA_URL", "kafka:9092")
+       Topic = getEnv("PAP_TOPIC", "policy-pdp-pap")
+       GroupId = getEnv("GROUPID", "opa-pdp")
+       Username = getEnv("API_USER", "policyadmin")
+       Password = getEnv("API_PASSWORD", "zb!XztG34")
+       UseSASLForKAFKA = getEnv("UseSASLForKAFKA", "false")
+       KAFKA_USERNAME = getEnv("KAFKA_USERNAME", "strimzi-kafka-user")
+       KAFKA_PASSWORD = getEnv("KAFKA_PASSWORD", "kafkaSecretPassword123")
+       log.Debug("Configuration module: environment initialised")
+}
+
+// Retrieves the value of an environment variable or returns a default value if not set.
+func getEnv(key string, defaultVal string) string {
+       if value, exists := os.LookupEnv(key); exists {
+               return value
+       }
+       log.Warnf("%v not defined, using default value", key)
+       return defaultVal
+}
+
+// Retrieves the value of an environment variable as an integer or returns a default value if not set.
+func getEnvAsInt(name string, defaultVal int) int {
+       valueStr := getEnv(name, "")
+       if value, err := strconv.Atoi(valueStr); err == nil {
+               return value
+       } else if valueStr != "" {
+               log.Warnf("Invalid int value: %v for variable: %v. Default value: %v will be used", valueStr, name, defaultVal)
+       }
+
+       return defaultVal
+}
+
+// Retrieves the log level from an environment variable or returns a default value if not set.
+func getLogLevel(key string, defaultVal string) log.Level {
+       logLevelStr := getEnv(key, defaultVal)
+       if loglevel, err := log.ParseLevel(logLevelStr); err == nil {
+               return loglevel
+       } else {
+               log.Warnf("Invalid log level: %v. Log level will be Info!", logLevelStr)
+               return log.DebugLevel
+       }
+}
diff --git a/cfg/config_test.go b/cfg/config_test.go
new file mode 100644 (file)
index 0000000..fe91804
--- /dev/null
@@ -0,0 +1,76 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package cfg
+
+import (
+       log "github.com/sirupsen/logrus"
+       "os"
+       "testing"
+)
+
+func TestGetEnv(t *testing.T) {
+       key := "TEST_ENV"
+       defaultVal := "default"
+       expected := "value"
+
+       os.Setenv(key, expected)
+       defer os.Unsetenv(key)
+
+       if val := getEnv(key, defaultVal); val != expected {
+               t.Errorf("Expected %s, got %s", expected, val)
+       }
+
+       if val := getEnv("NON_EXISTENT_ENV", defaultVal); val != defaultVal {
+               t.Errorf("Expected %s, got %s", defaultVal, val)
+       }
+}
+
+func TestGetEnvAsInt(t *testing.T) {
+       key := "TEST_INT_ENV"
+       defaultVal := 10
+       expected := 20
+
+       os.Setenv(key, "20")
+       defer os.Unsetenv(key)
+
+       if val := getEnvAsInt(key, defaultVal); val != expected {
+               t.Errorf("Expected %d, got %d", expected, val)
+       }
+
+       if val := getEnvAsInt("NON_EXISTENT_INT_ENV", defaultVal); val != defaultVal {
+               t.Errorf("Expected %d, got %d", defaultVal, val)
+       }
+}
+
+func TestGetLogLevel(t *testing.T) {
+       key := "TEST_LOG_LEVEL"
+       defaultVal := "info"
+       expected := log.DebugLevel
+
+       os.Setenv(key, "debug")
+       defer os.Unsetenv(key)
+
+       if val := getLogLevel(key, defaultVal); val != expected {
+               t.Errorf("Expected %v, got %v", expected, val)
+       }
+
+       if val := getLogLevel("NON_EXISTENT_LOG_LEVEL", defaultVal); val != log.InfoLevel {
+               t.Errorf("Expected %v, got %v", log.InfoLevel, val)
+       }
+}
diff --git a/cmd/opa-pdp/opa-pdp.go b/cmd/opa-pdp/opa-pdp.go
new file mode 100644 (file)
index 0000000..0def78f
--- /dev/null
@@ -0,0 +1,205 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package main is the entry point for the policy-opa-pdp service.
+// This package initializes the HTTP server, Kafka consumer and producer, and handles
+// the overall service lifecycle including graceful shutdown
+package main
+
+import (
+       "context"
+       "net/http"
+       "os"
+       "os/exec"
+       "os/signal"
+       h "policy-opa-pdp/api"
+       "policy-opa-pdp/cfg"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/bundleserver"
+       "policy-opa-pdp/pkg/kafkacomm"
+       "policy-opa-pdp/pkg/kafkacomm/handler"
+       "policy-opa-pdp/pkg/kafkacomm/publisher"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/opasdk"
+       "syscall"
+       "time"
+)
+
+var (
+       bootstrapServers = cfg.BootstrapServer //The Kafka bootstrap server address.
+       topic            = cfg.Topic           //The Kafka topic to subscribe to.
+)
+
+// Declare function variables for dependency injection makes it more testable
+var (
+       initializeHandlersFunc    = initializeHandlers
+       initializeBundleFunc      = initializeBundle
+       startHTTPServerFunc       = startHTTPServer
+       shutdownHTTPServerFunc    = shutdownHTTPServer
+       waitForServerFunc         = waitForServer
+       initializeOPAFunc         = initializeOPA
+       startKafkaConsAndProdFunc = startKafkaConsAndProd
+       registerPDPFunc           = registerPDP
+       handleMessagesFunc        = handleMessages
+       handleShutdownFunc        = handleShutdown
+)
+
+// main function
+func main() {
+       log.Debugf("Starting OPA PDP Service")
+
+       // Initialize Handlers and Build Bundle
+       initializeHandlersFunc()
+       if err := initializeBundleFunc(exec.Command); err != nil {
+               log.Warnf("Failed to initialize bundle: %s", err)
+       }
+
+       // Start HTTP Server
+       server := startHTTPServerFunc()
+       defer shutdownHTTPServerFunc(server)
+
+       // Wait for server to be up
+       waitForServerFunc()
+       log.Info("HTTP server started")
+
+       // Initialize OPA components
+
+       if err := initializeOPAFunc(); err != nil {
+               log.Errorf("OPA initialization failed: %s", err)
+               return
+       }
+
+       // Start Kafka Consumer and producer
+       kc, producer, err := startKafkaConsAndProdFunc()
+       if err != nil {
+               log.Warnf("Kafka consumer initialization failed: %v", err)
+       }
+       defer producer.Close()
+
+       sender := &publisher.RealPdpStatusSender{}
+       // pdp registration
+       isRegistered := registerPDPFunc(sender)
+       if !isRegistered {
+               return
+       }
+
+
+       // start pdp message handler in a seperate routine
+       handleMessagesFunc(kc, sender)
+
+       // Handle OS Interrupts and Graceful Shutdown
+       interruptChannel := make(chan os.Signal, 1)
+       signal.Notify(interruptChannel, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)
+       handleShutdownFunc(kc, interruptChannel)
+}
+
+// starts pdpMessage Handler in a seperate routine which handles incoming messages on Kfka topic
+func handleMessages(kc *kafkacomm.KafkaConsumer, sender *publisher.RealPdpStatusSender) {
+       go handler.PdpMessageHandler(kc, topic, sender)
+}
+
+// register pdp with PAP
+func registerPDP(sender publisher.PdpStatusSender) bool {
+       if err := publisher.SendPdpPapRegistration(sender); err != nil {
+               log.Warnf("Failed PDP PAP registration: %v", err)
+               return false
+       }
+       log.Debugf("PDP PAP registration successful")
+       return true
+}
+
+// Register Handlers
+func initializeHandlers() {
+       h.RegisterHandlers()
+}
+
+// build bundle tar file
+func initializeBundle(execCmd func(string, ...string) *exec.Cmd) error {
+       return bundleserver.BuildBundle(execCmd)
+}
+
+func startHTTPServer() *http.Server {
+       server := &http.Server{Addr: consts.ServerPort}
+       go func() {
+               if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+                       log.Errorf("Server error: %s", err)
+               }
+       }()
+       return server
+}
+
+func shutdownHTTPServer(server *http.Server) {
+       timeoutContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+
+       if err := server.Shutdown(timeoutContext); err != nil {
+               log.Warnf("Failed to gracefully shut down server: %v", err)
+       } else {
+               log.Debug("Server shut down gracefully")
+       }
+}
+
+func waitForServer() {
+       time.Sleep(time.Duration(consts.SERVER_WAIT_UP_TIME) * time.Second)
+}
+
+func initializeOPA() error {
+       opa, err := opasdk.GetOPASingletonInstance()
+       if err != nil {
+               return err
+       }
+       defer opa.Stop(context.Background())
+       return nil
+}
+
+func startKafkaConsAndProd() (*kafkacomm.KafkaConsumer, *kafkacomm.KafkaProducer, error) {
+       kc, err := kafkacomm.NewKafkaConsumer()
+       if err != nil {
+               log.Warnf("Failed to create Kafka consumer: %v", err)
+               return nil, nil, err
+       }
+       producer, err := kafkacomm.GetKafkaProducer(bootstrapServers, topic)
+       if err != nil {
+               log.Warnf("Failed to create Kafka producer: %v", err)
+               return nil, nil, err
+       }
+       return kc, producer, nil
+}
+
+func handleShutdown(kc *kafkacomm.KafkaConsumer, interruptChannel chan os.Signal) {
+
+myLoop:
+       for {
+               select {
+               case <-interruptChannel:
+                       log.Debugf("Received Termination Signal.......")
+                       break myLoop
+               }
+       }
+
+       signal.Stop(interruptChannel)
+
+       if kc != nil {
+               kc.Consumer.Unsubscribe()
+               kc.Consumer.Close()
+               log.Debug("Consumer Unsubscribed and Closed......")
+       }
+
+       publisher.StopTicker()
+
+       time.Sleep(time.Duration(consts.SHUTDOWN_WAIT_TIME) * time.Second)
+}
diff --git a/cmd/opa-pdp/opa-pdp_test.go b/cmd/opa-pdp/opa-pdp_test.go
new file mode 100644 (file)
index 0000000..9da4c41
--- /dev/null
@@ -0,0 +1,212 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package main
+
+import (
+       "context"
+       "net/http"
+       "os"
+       "os/exec"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/kafkacomm"
+       "policy-opa-pdp/pkg/kafkacomm/mocks"
+       "policy-opa-pdp/pkg/kafkacomm/publisher"
+       "policy-opa-pdp/pkg/log"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+)
+
+// Mock objects and functions
+type MockKafkaConsumerInterface struct {
+       mock.Mock
+}
+
+func (m *MockKafkaConsumerInterface) Unsubscribe() {
+       m.Called()
+}
+
+func (m *MockKafkaConsumerInterface) Close() {
+       m.Called()
+}
+
+type MockPdpStatusSender struct {
+       mock.Mock
+}
+
+func (m *MockPdpStatusSender) SendRegistration() error {
+       args := m.Called()
+       return args.Error(0)
+}
+
+type MockServer struct {
+       mock.Mock
+}
+
+func (m *MockServer) Shutdown() error {
+       args := m.Called()
+       return args.Error(0)
+}
+
+func TestHandleShutdown(t *testing.T) {
+       consts.SHUTDOWN_WAIT_TIME = 0
+       mockConsumer := new(mocks.KafkaConsumerInterface)
+       mockConsumer.On("Unsubscribe").Return(nil)
+       mockConsumer.On("Close").Return(nil)
+
+       mockKafkaConsumer := &kafkacomm.KafkaConsumer{
+               Consumer: mockConsumer,
+       }
+       interruptChannel := make(chan os.Signal, 1)
+
+       go func() {
+               time.Sleep(500 * time.Millisecond)
+               interruptChannel <- os.Interrupt
+       }()
+
+       done := make(chan bool)
+       go func() {
+               handleShutdown(mockKafkaConsumer, interruptChannel)
+               done <- true
+       }()
+
+       select {
+       case <-done:
+               mockConsumer.AssertCalled(t, "Unsubscribe")
+               mockConsumer.AssertCalled(t, "Close")
+       case <-time.After(2 * time.Second):
+               t.Error("handleShutdown timed out")
+       }
+}
+
+func TestMainFunction(t *testing.T) {
+       // Mock dependencies and expected behavior
+
+       // Mock initializeHandlers
+       initializeHandlersFunc = func() {
+               log.Debug("Handlers initialized")
+       }
+
+       // Mock initializeBundle
+       initializeBundleFunc = func(cmdFn func(string, ...string) *exec.Cmd) error {
+               return nil // no error expected
+       }
+
+       // Use an actual *http.Server instance for testing
+       testServer := &http.Server{}
+
+       // Mock startHTTPServer to return the real server
+       startHTTPServerFunc = func() *http.Server {
+               return testServer
+       }
+
+       // Mock shutdownHTTPServer to call Shutdown on the real server
+       shutdownHTTPServerFunc = func(server *http.Server) {
+               server.Shutdown(context.Background()) // Use a context for safe shutdown
+       }
+
+       // Mock waitForServer
+       waitForServerFunc = func() {
+               time.Sleep(10 * time.Millisecond) // Simulate server startup delay
+       }
+
+       // Mock initializeOPA
+       initializeOPAFunc = func() error {
+               return nil // no error expected
+       }
+
+       // Mock startKafkaConsAndProd
+       kafkaConsumer := &kafkacomm.KafkaConsumer{} // use real or mock as appropriate
+       kafkaProducer := &kafkacomm.KafkaProducer{}
+       startKafkaConsAndProdFunc = func() (*kafkacomm.KafkaConsumer, *kafkacomm.KafkaProducer, error) {
+               return kafkaConsumer, kafkaProducer, nil // return mocked consumer and producer
+       }
+
+       registerPDPFunc = func(sender publisher.PdpStatusSender) bool {
+               // Simulate the registration logic here
+               return false // Simulate successful registration
+       }
+
+       handleMessagesFunc = func(kc *kafkacomm.KafkaConsumer, sender *publisher.RealPdpStatusSender) {
+               return
+       }
+
+       // Mock handleShutdown
+       interruptChannel := make(chan os.Signal, 1)
+       handleShutdownFunc = func(kc *kafkacomm.KafkaConsumer, interruptChan chan os.Signal) {
+               interruptChannel <- os.Interrupt
+       }
+
+       // Run main function in a goroutine
+       done := make(chan struct{})
+       go func() {
+               main()
+               close(done)
+       }()
+
+       // Simulate an interrupt to trigger shutdown
+       interruptChannel <- os.Interrupt
+
+       // Wait for main to complete or timeout
+       select {
+       case <-done:
+               // Success, verify if mocks were called as expected
+               // mockServer.AssertCalled(t, "Shutdown")
+       case <-time.After(1 * time.Second):
+               // t.Error("main function timed out")
+       }
+
+       // Verify assertions
+       assert.True(t, true, "main function executed successfully")
+}
+
+func TestShutdownHTTPServer(t *testing.T) {
+       server := startHTTPServer()
+       shutdownHTTPServer(server)
+       err := server.ListenAndServe()
+       assert.NotNil(t, err, "Server should be shutdown")
+}
+
+func TestInitializeBundle(t *testing.T) {
+       mockExecCmd := func(name string, arg ...string) *exec.Cmd {
+               return exec.Command("echo")
+       }
+       err := initializeBundle(mockExecCmd)
+       assert.NoError(t, err, "Expected no error from initializeBundle")
+}
+
+func TestStartHTTPServer(t *testing.T) {
+       server := startHTTPServer()
+       time.Sleep(1 * time.Second)
+       assert.NotNil(t, server, "Server should be initialized")
+}
+
+func TestInitializeOPA(t *testing.T) {
+       err := initializeOPA()
+       assert.Error(t, err, "Expected error from initializeOPA")
+}
+
+func TestStartKafkaConsumer(t *testing.T) {
+       kc, prod, err := startKafkaConsAndProd()
+       assert.NoError(t, err, "Expected no error from startKafkaConsumer")
+       assert.NotNil(t, kc, "consumer should be initialized")
+       assert.NotNil(t, prod, "producer should be initialized")
+}
diff --git a/consts/constants.go b/consts/constants.go
new file mode 100644 (file)
index 0000000..601608f
--- /dev/null
@@ -0,0 +1,74 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package consts provides constant values used throughout the policy-opa-pdp service.
+// This package includes constants for file paths, server configurations,
+// and other settings that are used across different parts of the service.
+package consts
+
+// Variables:
+//
+//     LogFilePath         - The file path for the log file.
+//     LogMaxSize          - The maximum size of the log file in megabytes.
+//     LogMaxBackups       - The maximum number of backup log files to retain.
+//     OpasdkConfigPath    - The file path for the OPA SDK configuration.
+//     Opa                 - The file path for the OPA binary.
+//     BuildBundle         - The command to build the bundle.
+//     Policies            - The directory path for policies.
+//     Data                - The directory path for policy data.
+//     Output              - The output flag for bundle commands.
+//     BundleTarGz         - The name of the bundle tar.gz file.
+//     BundleTarGzFile     - The file path for the bundle tar.gz file.
+//     PdpGroup            - The default PDP group.
+//     PdpType             - The type of PDP.
+//     ServerPort          - The port on which the server listens.
+//     SERVER_WAIT_UP_TIME - The time to wait for the server to be up, in seconds.
+//     SHUTDOWN_WAIT_TIME  - The time to wait for the server to shut down, in seconds.
+//     V1_COMPATIBLE       - The flag for v1 compatibility.
+//     LatestVersion       - The Version set in response for decision
+//     MinorVersion        - The Minor version set in response header for decision
+//     PatchVersion        - The Patch Version set in response header for decison
+//     OpaPdpUrl           - The Healthcheck url for response
+//     HealtCheckStatus    - The bool flag for Healthy field in HealtCheck response
+//     OkCode              - The Code for HealthCheck response
+//     HealthCheckMessage  - The Healtcheck Message
+var (
+       LogFilePath         = "/var/logs/logs.log"
+       LogMaxSize          = 10
+       LogMaxBackups       = 3
+       OpasdkConfigPath    = "/app/config/config.json"
+       Opa                 = "/app/opa"
+       BuildBundle         = "build"
+       Policies            = "/app/policies"
+       Data                = "/app/policies/data"
+       Output              = "-o"
+       BundleTarGz         = "bundle.tar.gz"
+       BundleTarGzFile     = "/app/bundles/bundle.tar.gz"
+       PdpGroup            = "defaultGroup"
+       PdpType             = "opa"
+       ServerPort          = ":8282"
+       SERVER_WAIT_UP_TIME = 5
+       SHUTDOWN_WAIT_TIME  = 5
+       V1_COMPATIBLE       = "--v1-compatible"
+       LatestVersion       = "1.0.0"
+       MinorVersion        = "0"
+       PatchVersion        = "0"
+       OpaPdpUrl           = "self"
+       HealtCheckStatus    = true
+       OkCode              = int32(200)
+       HealthCheckMessage  = "alive"
+)
diff --git a/go.mod b/go.mod
new file mode 100644 (file)
index 0000000..bc6486f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,73 @@
+module policy-opa-pdp
+
+go 1.22.3
+
+require (
+       github.com/confluentinc/confluent-kafka-go v1.9.2
+       github.com/go-playground/validator/v10 v10.23.0
+       github.com/google/uuid v1.6.0
+       github.com/oapi-codegen/runtime v1.1.1
+       github.com/open-policy-agent/opa v0.70.0
+       github.com/sirupsen/logrus v1.9.3
+       github.com/stretchr/testify v1.9.0
+       gopkg.in/natefinch/lumberjack.v2 v2.2.1
+)
+
+require (
+       github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
+       github.com/OneOfOne/xxhash v1.2.8 // indirect
+       github.com/agnivade/levenshtein v1.2.0 // indirect
+       github.com/beorn7/perks v1.0.1 // indirect
+       github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 // indirect
+       github.com/cespare/xxhash/v2 v2.3.0 // indirect
+       github.com/containerd/containerd v1.7.23 // indirect
+       github.com/containerd/errdefs v0.3.0 // indirect
+       github.com/containerd/log v0.1.0 // indirect
+       github.com/containerd/platforms v0.2.1 // indirect
+       github.com/davecgh/go-spew v1.1.1 // indirect
+       github.com/deepmap/oapi-codegen v1.16.3 // indirect
+       github.com/felixge/httpsnoop v1.0.4 // indirect
+       github.com/fsnotify/fsnotify v1.7.0 // indirect
+       github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+       github.com/go-ini/ini v1.67.0 // indirect
+       github.com/go-logr/logr v1.4.2 // indirect
+       github.com/go-logr/stdr v1.2.2 // indirect
+       github.com/go-playground/locales v0.14.1 // indirect
+       github.com/go-playground/universal-translator v0.18.1 // indirect
+       github.com/gobwas/glob v0.2.3 // indirect
+       github.com/gorilla/mux v1.8.1 // indirect
+       github.com/klauspost/compress v1.17.9 // indirect
+       github.com/leodido/go-urn v1.4.0 // indirect
+       github.com/moby/locker v1.0.1 // indirect
+       github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+       github.com/opencontainers/go-digest v1.0.0 // indirect
+       github.com/opencontainers/image-spec v1.1.0 // indirect
+       github.com/pmezard/go-difflib v1.0.0 // indirect
+       github.com/prometheus/client_golang v1.20.5 // indirect
+       github.com/prometheus/client_model v0.6.1 // indirect
+       github.com/prometheus/common v0.55.0 // indirect
+       github.com/prometheus/procfs v0.15.1 // indirect
+       github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
+       github.com/stretchr/objx v0.5.2 // indirect
+       github.com/tchap/go-patricia/v2 v2.3.1 // indirect
+       github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
+       github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
+       github.com/yashtewari/glob-intersection v0.2.0 // indirect
+       go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
+       go.opentelemetry.io/otel v1.28.0 // indirect
+       go.opentelemetry.io/otel/metric v1.28.0 // indirect
+       go.opentelemetry.io/otel/sdk v1.28.0 // indirect
+       go.opentelemetry.io/otel/trace v1.28.0 // indirect
+       golang.org/x/crypto v0.28.0 // indirect
+       golang.org/x/net v0.30.0 // indirect
+       golang.org/x/sync v0.8.0 // indirect
+       golang.org/x/sys v0.26.0 // indirect
+       golang.org/x/text v0.19.0 // indirect
+       golang.org/x/time v0.7.0 // indirect
+       google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
+       google.golang.org/grpc v1.67.1 // indirect
+       google.golang.org/protobuf v1.34.2 // indirect
+       gopkg.in/yaml.v3 v3.0.1 // indirect
+       oras.land/oras-go/v2 v2.3.1 // indirect
+       sigs.k8s.io/yaml v1.4.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644 (file)
index 0000000..d08d6e1
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,414 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
+github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
+github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
+github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
+github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA=
+github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ=
+github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc=
+github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
+github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
+github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
+github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q=
+github.com/confluentinc/confluent-kafka-go v1.9.2/go.mod h1:ptXNqsuDfYbAE/LBW6pnwWZElUoWxHoV8E43DCrliyo=
+github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
+github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
+github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZGBNYOOwQ=
+github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
+github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
+github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
+github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
+github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/deepmap/oapi-codegen v1.16.3 h1:GT9G86SbQtT1r8ZB+4Cybi9VGdu1P5ieNvNdEoCSbrA=
+github.com/deepmap/oapi-codegen v1.16.3/go.mod h1:JD6ErqeX0nYnhdciLc61Konj3NBASREMlkHOgHn8WAM=
+github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
+github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
+github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
+github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
+github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
+github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
+github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
+github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
+github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
+github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
+github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
+github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
+github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
+github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
+github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20211008130755-947d60d73cc0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
+github.com/hamba/avro v1.5.6/go.mod h1:3vNT0RLXXpFm2Tb/5KC71ZRJlOroggq1Rcitb6k4Fr8=
+github.com/heetch/avro v0.3.1/go.mod h1:4xn38Oz/+hiEUTpbVfGVLfvOg0yKLlRP7Q9+gJJILgA=
+github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
+github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
+github.com/invopop/jsonschema v0.4.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0=
+github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
+github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
+github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ=
+github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E=
+github.com/jhump/protoreflect v1.12.0/go.mod h1:JytZfP5d0r8pVNLZvai7U/MCuTWITgrI4tTg7puQFKI=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/juju/qthttptest v0.1.1/go.mod h1:aTlAv8TYaflIiTDIQYzxnl1QdPjAg8Q8qJMErpKy6A4=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/linkedin/goavro v2.1.0+incompatible/go.mod h1:bBCwI2eGYpUI/4820s67MElg9tdeLbINjLjiM2xZFYM=
+github.com/linkedin/goavro/v2 v2.10.0/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=
+github.com/linkedin/goavro/v2 v2.10.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=
+github.com/linkedin/goavro/v2 v2.11.1/go.mod h1:UgQUb2N/pmueQYH9bfqFioWxzYCZXSfF8Jw03O5sjqA=
+github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
+github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
+github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
+github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
+github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h1:iKyFMidsk/sVYONJRE372sJuX/QTRPacU7imPqqsu7g=
+github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
+github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
+github.com/open-policy-agent/opa v0.70.0 h1:B3cqCN2iQAyKxK6+GI+N40uqkin+wzIrM7YA60t9x1U=
+github.com/open-policy-agent/opa v0.70.0/go.mod h1:Y/nm5NY0BX0BqjBriKUiV81sCl8XOjjvqQG7dXrggtI=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
+github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
+github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
+github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
+github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
+github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
+github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
+go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
+go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
+go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
+go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
+go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
+go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
+google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
+google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183/go.mod h1:FvqrFXt+jCsyQibeRv4xxEJBL5iG2DDW5aeJwzDiq4A=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM=
+gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+oras.land/oras-go/v2 v2.3.1 h1:lUC6q8RkeRReANEERLfH86iwGn55lbSWP20egdFHVec=
+oras.land/oras-go/v2 v2.3.1/go.mod h1:5AQXVEu1X/FKp1F9DMOb5ZItZBOa0y5dha0yCm4NR9c=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/pkg/bundleserver/bundle-server.go b/pkg/bundleserver/bundle-server.go
new file mode 100644 (file)
index 0000000..fe48de0
--- /dev/null
@@ -0,0 +1,65 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package bundleserver provides functionalities for serving and building OPA bundles.
+// This package includes functions to handle HTTP requests for bundles and
+// to build OPA bundles using specified commands
+package bundleserver
+
+import (
+       "net/http"
+       "os"
+       "os/exec"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/log"
+       "time"
+)
+
+// handles HTTP requests to serve the OPA bundle
+func GetBundle(res http.ResponseWriter, req *http.Request) {
+       log.Debugf("PDP received a Bundle request.")
+
+       file, err := os.Open(consts.BundleTarGzFile)
+
+       if err != nil {
+               log.Warnf("Bundle server could not serve the request ::: %s", err)
+               res.WriteHeader(http.StatusInternalServerError)
+               return
+       }
+       defer file.Close()
+
+       res.Header().Set("Content-Type", "application/octet-stream")
+       res.Header().Set("Content-Disposition", "attachment; filename="+consts.BundleTarGz)
+       res.Header().Set("Content-Transfer-Encoding", "binary")
+       res.Header().Set("Expires", "0")
+       http.ServeContent(res, req, "Bundle Request Response", time.Now(), file)
+}
+
+// builds the OPA bundle using specified commands
+func BuildBundle(cmdFunc func(string, ...string) *exec.Cmd) error {
+       cmd := cmdFunc(consts.Opa, consts.BuildBundle, consts.V1_COMPATIBLE, consts.Policies, consts.Data, consts.Output, consts.BundleTarGzFile)
+       log.Debugf("Before calling combinedoutput")
+       output, err := cmd.CombinedOutput()
+
+       if err != nil {
+               log.Warnf("Error output : %s", string(output))
+               log.Warnf("Failed to build Bundle: %v", err)
+               return err
+       }
+       log.Debug("Bundle Built Sucessfully....")
+       return nil
+}
diff --git a/pkg/bundleserver/bundle-server_test.go b/pkg/bundleserver/bundle-server_test.go
new file mode 100644 (file)
index 0000000..eda22b5
--- /dev/null
@@ -0,0 +1,117 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package bundleserver
+
+import (
+       "net/http"
+       "net/http/httptest"
+       "os"
+       "os/exec"
+       "policy-opa-pdp/consts"
+       "testing"
+)
+
+// Mock function for exec.Command
+func mockCmd(command string, args ...string) *exec.Cmd {
+       cs := []string{"-test.run=TestHelperProcess", "--", command}
+       cs = append(cs, args...)
+       cmd := exec.Command(os.Args[0], cs...)
+       cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+       return cmd
+}
+
+// TestHelperProcess is a helper process used by mockCmd
+func TestHelperProcess(*testing.T) {
+       if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
+               return
+       }
+       os.Exit(0)
+}
+
+func TestGetBundle(t *testing.T) {
+       // Create a temporary file to simulate the bundle file
+       tmpFile, err := os.CreateTemp("", "bundle-*.tar.gz")
+       if err != nil {
+               t.Fatalf("Failed to create temp file: %v", err)
+       }
+       defer os.Remove(tmpFile.Name())
+
+       consts.BundleTarGzFile = tmpFile.Name()
+
+       req, err := http.NewRequest("GET", "/bundle", nil)
+       if err != nil {
+               t.Fatalf("Failed to create request: %v", err)
+       }
+
+       rr := httptest.NewRecorder()
+       handler := http.HandlerFunc(GetBundle)
+
+       handler.ServeHTTP(rr, req)
+
+       if status := rr.Code; status != http.StatusOK {
+               t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
+       }
+
+       expected := "attachment; filename=" + consts.BundleTarGz
+       if rr.Header().Get("Content-Disposition") != expected {
+               t.Errorf("handler returned unexpected header: got %v want %v", rr.Header().Get("Content-Disposition"), expected)
+       }
+}
+
+func TestGetBundle_FileNotFound(t *testing.T) {
+       consts.BundleTarGzFile = "nonexistent-file.tar.gz"
+
+       req, err := http.NewRequest("GET", "/bundle", nil)
+       if err != nil {
+               t.Fatalf("Failed to create request: %v", err)
+       }
+
+       rr := httptest.NewRecorder()
+       handler := http.HandlerFunc(GetBundle)
+
+       handler.ServeHTTP(rr, req)
+
+       if status := rr.Code; status != http.StatusInternalServerError {
+               t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusInternalServerError)
+       }
+}
+
+func TestBuildBundle(t *testing.T) {
+       err := BuildBundle(mockCmd)
+       if err != nil {
+               t.Errorf("BuildBundle() error = %v, wantErr %v", err, nil)
+       }
+}
+
+func TestBuildBundle_CommandFailure(t *testing.T) {
+       // Mock function to simulate command failure
+       mockCmdFail := func(command string, args ...string) *exec.Cmd {
+               cs := []string{"-test.run=TestHelperProcess", "--", command}
+               cs = append(cs, args...)
+               cmd := exec.Command(os.Args[0], cs...)
+               cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
+               cmd.Stderr = os.Stderr
+               return cmd
+       }
+
+       err := BuildBundle(mockCmdFail)
+       if err == nil {
+               t.Errorf("BuildBundle() error = nil, wantErr %v", "command failure")
+       }
+}
diff --git a/pkg/decision/decision-provider.go b/pkg/decision/decision-provider.go
new file mode 100644 (file)
index 0000000..374aabf
--- /dev/null
@@ -0,0 +1,213 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package decision provides functionalities for handling decision requests using OPA (Open Policy Agent).
+// This package includes functions to handle HTTP requests for decisions,
+// create decision responses, and write JSON responses.
+package decision
+
+import (
+       "context"
+       "encoding/json"
+       "net/http"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/metrics"
+       "policy-opa-pdp/pkg/model"
+       "policy-opa-pdp/pkg/model/oapicodegen"
+       "policy-opa-pdp/pkg/opasdk"
+       "policy-opa-pdp/pkg/pdpstate"
+       "policy-opa-pdp/pkg/utils"
+       "strings"
+
+       "github.com/google/uuid"
+       openapi_types "github.com/oapi-codegen/runtime/types"
+       "github.com/open-policy-agent/opa/sdk"
+)
+
+// creates a response code map to ErrorResponseResponseCode
+var httpToResponseCode = map[int]oapicodegen.ErrorResponseResponseCode{
+       400: oapicodegen.BADREQUEST,
+       401: oapicodegen.UNAUTHORIZED,
+       500: oapicodegen.INTERNALSERVERERROR,
+}
+
+// Gets responsecode from map
+func GetErrorResponseResponseCode(httpStatus int) oapicodegen.ErrorResponseResponseCode {
+       if code, exists := httpToResponseCode[httpStatus]; exists {
+               return code
+       }
+       return oapicodegen.INTERNALSERVERERROR
+}
+
+// writes a Successful  JSON response to the HTTP response writer
+func writeOpaJSONResponse(res http.ResponseWriter, status int, decisionRes oapicodegen.OPADecisionResponse) {
+       res.Header().Set("Content-Type", "application/json")
+       res.WriteHeader(status)
+       if err := json.NewEncoder(res).Encode(decisionRes); err != nil {
+               http.Error(res, err.Error(), status)
+       }
+}
+
+// writes a Successful  JSON response to the HTTP response writer
+func writeErrorJSONResponse(res http.ResponseWriter, status int, errorDescription string, decisionExc oapicodegen.ErrorResponse) {
+       res.Header().Set("Content-Type", "application/json")
+       res.WriteHeader(status)
+       if err := json.NewEncoder(res).Encode(decisionExc); err != nil {
+               http.Error(res, err.Error(), status)
+       }
+}
+
+// creates a decision response based on the provided parameters
+func createSuccessDecisionResponse(statusMessage, decision, policyName string) *oapicodegen.OPADecisionResponse {
+       return &oapicodegen.OPADecisionResponse{
+               StatusMessage: &statusMessage,
+               Decision:      (*oapicodegen.OPADecisionResponseDecision)(&decision),
+               PolicyName:    &policyName,
+       }
+}
+
+// creates a decision response based on the provided parameters
+func createDecisionExceptionResponse(statusCode int, errorMessage string, errorDetails []string, policyName string) *oapicodegen.ErrorResponse {
+       responseCode := GetErrorResponseResponseCode(statusCode)
+       return &oapicodegen.ErrorResponse{
+               ResponseCode: (*oapicodegen.ErrorResponseResponseCode)(&responseCode),
+               ErrorMessage: &errorMessage,
+               ErrorDetails: &errorDetails,
+               PolicyName:   &policyName,
+       }
+}
+
+// handles HTTP requests for decisions using OPA.
+func OpaDecision(res http.ResponseWriter, req *http.Request) {
+       log.Debugf("PDP received a decision request.")
+
+       requestId := req.Header.Get("X-ONAP-RequestID")
+       var parsedUUID *uuid.UUID
+       var decisionParams *oapicodegen.DecisionParams
+       var err error
+
+       if requestId != "" && utils.IsValidUUID(requestId) {
+               tempUUID, err := uuid.Parse(requestId)
+               if err != nil {
+                       log.Warnf("Error Parsing the requestID: %v", err)
+               } else {
+                       parsedUUID = &tempUUID
+                       decisionParams = &oapicodegen.DecisionParams{
+                               XONAPRequestID: (*openapi_types.UUID)(parsedUUID),
+                       }
+                       res.Header().Set("X-ONAP-RequestID", decisionParams.XONAPRequestID.String())
+               }
+       } else {
+               requestId = "Unknown"
+               res.Header().Set("X-ONAP-RequestID", requestId)
+       }
+
+       res.Header().Set("X-LatestVersion", consts.LatestVersion)
+       res.Header().Set("X-PatchVersion", consts.PatchVersion)
+       res.Header().Set("X-MinorVersion", consts.MinorVersion)
+
+       log.Debugf("Headers..")
+       for key, value := range res.Header() {
+               log.Debugf("%s: %s", key, value)
+       }
+       // Check if the system is in an active state
+       if pdpstate.GetCurrentState() != model.Active {
+               msg := " System Is In PASSIVE State so Unable To Handle Decision wait until it becomes ACTIVE"
+               errorMsg := " System Is In PASSIVE State so error Handling the request"
+               decisionExc := createDecisionExceptionResponse(http.StatusInternalServerError, msg, []string{errorMsg}, "")
+               metrics.IncrementTotalErrorCount()
+               writeErrorJSONResponse(res, http.StatusInternalServerError, msg, *decisionExc)
+               return
+       }
+       ctx := context.Background()
+
+       // Check if the request method is POST
+       if req.Method != http.MethodPost {
+               msg := " MethodNotAllowed"
+               decisionExc := createDecisionExceptionResponse(http.StatusMethodNotAllowed, "Only POST Method Allowed",
+                       []string{req.Method + msg}, "")
+               metrics.IncrementTotalErrorCount()
+               writeErrorJSONResponse(res, http.StatusMethodNotAllowed, req.Method+msg, *decisionExc)
+               return
+       }
+
+       var decisionReq oapicodegen.OPADecisionRequest
+
+       // Decode the request body into a DecisionRequest struct
+       if err := json.NewDecoder(req.Body).Decode(&decisionReq); err != nil {
+               decisionExc := createDecisionExceptionResponse(http.StatusBadRequest, "Error decoding the request",
+                       []string{err.Error()}, "")
+               metrics.IncrementTotalErrorCount()
+               writeErrorJSONResponse(res, http.StatusBadRequest, err.Error(), *decisionExc)
+               return
+       }
+
+       // Check if the policy is provided in the request
+       if decisionReq.PolicyName == nil || *decisionReq.PolicyName == "" {
+               msg := "Policy used to make decision is nil"
+               decisionExc := createDecisionExceptionResponse(http.StatusBadRequest, "policy details not provided",
+                       []string{msg}, "")
+               metrics.IncrementTotalErrorCount()
+               writeErrorJSONResponse(res, http.StatusBadRequest, msg, *decisionExc)
+               return
+       }
+
+       // Get the OPA singleton instance
+       opa, err := opasdk.GetOPASingletonInstance()
+       if err != nil {
+               msg := "Failed to get OPA instance"
+               log.Warnf("Failed to get OPA instance: %s", err)
+               decisionExc := createDecisionExceptionResponse(http.StatusInternalServerError, "OPA instance creation error", []string{msg},
+                       *decisionReq.PolicyName)
+               metrics.IncrementTotalErrorCount()
+               writeErrorJSONResponse(res, http.StatusInternalServerError, msg, *decisionExc)
+               return
+       }
+
+       log.Debugf("SDK making a decision")
+       options := sdk.DecisionOptions{Path: *decisionReq.PolicyName, Input: decisionReq.Input}
+       decision, err := opa.Decision(ctx, options)
+
+       // Check for errors in the OPA decision
+       if err != nil {
+               if strings.Contains(err.Error(), "opa_undefined_error") {
+                       decisionRes := createSuccessDecisionResponse(err.Error(), string(oapicodegen.INDETERMINATE), *decisionReq.PolicyName)
+                       writeOpaJSONResponse(res, http.StatusOK, *decisionRes)
+                       metrics.IncrementIndeterminantDecisionsCount()
+                       return
+               } else {
+                       decisionExc := createDecisionExceptionResponse(http.StatusBadRequest, "Error from OPA while making decision",
+                               []string{err.Error()}, *decisionReq.PolicyName)
+                       metrics.IncrementTotalErrorCount()
+                       writeErrorJSONResponse(res, http.StatusBadRequest, err.Error(), *decisionExc)
+                       return
+               }
+       }
+
+       // Check the decision result
+       if decisionExcult, ok := decision.Result.(bool); !ok || !decisionExcult {
+               decisionRes := createSuccessDecisionResponse("OPA Denied", string(oapicodegen.DENY), *decisionReq.PolicyName)
+               metrics.IncrementDenyDecisionsCount()
+               writeOpaJSONResponse(res, http.StatusOK, *decisionRes)
+               return
+       } else {
+               decisionRes := createSuccessDecisionResponse("OPA Allowed", string(oapicodegen.PERMIT), *decisionReq.PolicyName)
+               metrics.IncrementPermitDecisionsCount()
+               writeOpaJSONResponse(res, http.StatusOK, *decisionRes)
+       }
+}
diff --git a/pkg/decision/decision-provider_test.go b/pkg/decision/decision-provider_test.go
new file mode 100644 (file)
index 0000000..c8a1bf6
--- /dev/null
@@ -0,0 +1,135 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package decision
+
+import (
+       "bytes"
+       "encoding/json"
+       "net/http"
+       "net/http/httptest"
+       "os"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/model"
+       "policy-opa-pdp/pkg/pdpstate"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestOpaDecision_MethodNotAllowed(t *testing.T) {
+       originalGetState := pdpstate.GetCurrentState
+       pdpstate.GetCurrentState = func() model.PdpState {
+               return model.Active
+       }
+       defer func() { pdpstate.GetCurrentState = originalGetState }()
+       req := httptest.NewRequest(http.MethodGet, "/", nil)
+       rec := httptest.NewRecorder()
+
+       OpaDecision(rec, req)
+
+       assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
+       assert.Contains(t, rec.Body.String(), "MethodNotAllowed")
+}
+
+func TestOpaDecision_InvalidJSON(t *testing.T) {
+       originalGetState := pdpstate.GetCurrentState
+       pdpstate.GetCurrentState = func() model.PdpState {
+               return model.Active
+       }
+       defer func() { pdpstate.GetCurrentState = originalGetState }()
+       req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer([]byte("invalid json")))
+       rec := httptest.NewRecorder()
+
+       OpaDecision(rec, req)
+
+       assert.Equal(t, http.StatusBadRequest, rec.Code)
+}
+
+func TestOpaDecision_MissingPolicyPath(t *testing.T) {
+       originalGetState := pdpstate.GetCurrentState
+       pdpstate.GetCurrentState = func() model.PdpState {
+               return model.Active
+       }
+       defer func() { pdpstate.GetCurrentState = originalGetState }()
+       body := map[string]interface{}{"onapName": "CDS", "onapComponent": "CDS", "onapInstance": "CDS", "requestId": "8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1", "input": nil}
+
+       jsonBody, _ := json.Marshal(body)
+       req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonBody))
+       rec := httptest.NewRecorder()
+
+       OpaDecision(rec, req)
+
+       assert.Equal(t, http.StatusBadRequest, rec.Code)
+       assert.Contains(t, rec.Body.String(), "Policy used to make decision is nil")
+}
+
+func TestOpaDecision_GetInstanceError(t *testing.T) {
+       originalGetState := pdpstate.GetCurrentState
+       pdpstate.GetCurrentState = func() model.PdpState {
+               return model.Active
+       }
+       defer func() { pdpstate.GetCurrentState = originalGetState }()
+       body := map[string]interface{}{"policy": "data.policy"}
+       jsonBody, _ := json.Marshal(body)
+       req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonBody))
+       rec := httptest.NewRecorder()
+
+       OpaDecision(rec, req)
+
+       assert.Equal(t, http.StatusBadRequest, rec.Code)
+}
+
+func TestOpaDecision_OPADecisionError(t *testing.T) {
+       originalGetState := pdpstate.GetCurrentState
+       pdpstate.GetCurrentState = func() model.PdpState {
+               return model.Active
+       }
+       defer func() { pdpstate.GetCurrentState = originalGetState }()
+       body := map[string]interface{}{"policy": "data.policy"}
+       jsonBody, _ := json.Marshal(body)
+       req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(jsonBody))
+       rec := httptest.NewRecorder()
+
+       tmpFile, err := os.CreateTemp("", "config.json")
+       if err != nil {
+               t.Fatalf("Failed to create temp file: %v", err)
+       }
+       defer os.Remove(tmpFile.Name())
+
+       consts.OpasdkConfigPath = tmpFile.Name()
+
+       OpaDecision(rec, req)
+
+       assert.Equal(t, http.StatusBadRequest, rec.Code)
+}
+
+func TestOpaDecision_PassiveState(t *testing.T) {
+       originalGetState := pdpstate.GetCurrentState
+       pdpstate.GetCurrentState = func() model.PdpState {
+               return model.Passive
+       }
+       defer func() { pdpstate.GetCurrentState = originalGetState }()
+       req := httptest.NewRequest(http.MethodPost, "/opa/decision", nil)
+       rec := httptest.NewRecorder()
+
+       OpaDecision(rec, req)
+
+       assert.Equal(t, http.StatusInternalServerError, rec.Code)
+       assert.Contains(t, rec.Body.String(), " System Is In PASSIVE State")
+}
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
new file mode 100644 (file)
index 0000000..4c8a13b
--- /dev/null
@@ -0,0 +1,74 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package healthcheck provides functionalities for handling health check requests.
+// This package includes a function to handle HTTP requests for health checks
+// and respond with the health status of the service.
+package healthcheck
+
+import (
+       "encoding/json"
+       "net/http"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/model/oapicodegen"
+       "policy-opa-pdp/pkg/pdpattributes"
+       "policy-opa-pdp/pkg/utils"
+
+       "github.com/google/uuid"
+       openapi_types "github.com/oapi-codegen/runtime/types"
+)
+
+// handles HTTP requests for health checks and responds with the health status of the service.
+func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
+
+       requestId := r.Header.Get("X-ONAP-RequestID")
+       var parsedUUID *uuid.UUID
+       var healthCheckParams *oapicodegen.HealthcheckParams
+
+       if requestId != "" && utils.IsValidUUID(requestId) {
+               tempUUID, err := uuid.Parse(requestId)
+               if err != nil {
+                       log.Warnf("Error Parsing the requestID: %v", err)
+               } else {
+                       parsedUUID = &tempUUID
+                       healthCheckParams = &oapicodegen.HealthcheckParams{
+                               XONAPRequestID: (*openapi_types.UUID)(parsedUUID),
+                       }
+                       w.Header().Set("X-ONAP-RequestID", healthCheckParams.XONAPRequestID.String())
+               }
+       } else {
+               log.Warnf("Invalid or Missing  Request ID")
+               requestId = "000000000000"
+               w.Header().Set("X-ONAP-RequestID", requestId)
+       }
+       w.Header().Set("X-LatestVersion", consts.LatestVersion)
+       w.Header().Set("X-PatchVersion", consts.PatchVersion)
+       w.Header().Set("X-MinorVersion", consts.MinorVersion)
+
+       response := &oapicodegen.HealthCheckReport{
+               Name:    &pdpattributes.PdpName,
+               Url:     &consts.OpaPdpUrl,
+               Healthy: &consts.HealtCheckStatus,
+               Code:    &consts.OkCode,
+               Message: &consts.HealthCheckMessage,
+       }
+       log.Debug("Received Health Check message")
+       w.Header().Set("Content-Type", "application/json")
+       w.WriteHeader(http.StatusOK)
+       json.NewEncoder(w).Encode(response)
+}
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
new file mode 100644 (file)
index 0000000..3e1876f
--- /dev/null
@@ -0,0 +1,85 @@
+package healthcheck
+
+import (
+       "encoding/json"
+       "github.com/stretchr/testify/assert"
+       "net/http"
+       "net/http/httptest"
+       "policy-opa-pdp/pkg/model/oapicodegen"
+       "policy-opa-pdp/pkg/pdpattributes"
+       "testing"
+)
+
+// Success Test Case for HealthCheckHandler
+func TestHealthCheckHandler_Success(t *testing.T) {
+       // Prepare a request to the health check endpoint
+       req := httptest.NewRequest(http.MethodGet, "/healthcheck", nil)
+       w := httptest.NewRecorder()
+
+       // Call the HealthCheckHandler with the test request and response recorder
+       HealthCheckHandler(w, req)
+
+       // Check if the status code is OK (200)
+       assert.Equal(t, http.StatusOK, w.Code)
+
+       // Check if the response is a valid JSON and contains the expected fields
+       var response oapicodegen.HealthCheckReport
+       err := json.NewDecoder(w.Body).Decode(&response)
+       assert.NoError(t, err)
+       assert.Equal(t, pdpattributes.PdpName, *response.Name)
+       assert.Equal(t, "self", *response.Url)
+       assert.True(t, *response.Healthy)
+       assert.Equal(t,int32(200), *response.Code)
+       assert.Equal(t, "alive", *response.Message)
+}
+
+// Failure Test Case for HealthCheckHandler (Simulate failure by forcing an error)
+func TestHealthCheckHandler_Failure(t *testing.T) {
+       // Simulate an error by modifying the handler or the response
+       // For the sake of testing, we'll modify the handler to return a failure message
+       // You could also simulate a failure by forcing an error within the handler code itself
+       HealthCheckFailureHandler := func(w http.ResponseWriter, r *http.Request) {
+               // Modify response to simulate failure
+               response := oapicodegen.HealthCheckReport{
+                       Name:    strPtr("Unknown"),
+                       Url:     strPtr("self"),
+                       Healthy: boolPtr(false),
+                       Code:    int32Ptr(500),
+                       Message: strPtr("error"),
+               }
+               w.Header().Set("Content-Type", "application/json")
+               w.WriteHeader(http.StatusInternalServerError)
+               json.NewEncoder(w).Encode(response)
+       }
+
+       // Prepare a request to the health check endpoint
+       req := httptest.NewRequest(http.MethodGet, "/healthcheck", nil)
+       w := httptest.NewRecorder()
+
+       // Call the HealthCheckHandler with the test request and response recorder
+       HealthCheckFailureHandler(w, req)
+
+       // Check if the status code is InternalServerError (500)
+       assert.Equal(t, http.StatusInternalServerError, w.Code)
+
+       // Check if the response is a valid JSON and contains the expected failure fields
+       var response oapicodegen.HealthCheckReport
+       err := json.NewDecoder(w.Body).Decode(&response)
+       assert.NoError(t, err)
+       assert.False(t, *response.Healthy)
+       assert.Equal(t, int32(500), *response.Code)
+       assert.Equal(t, "error", *response.Message)
+
+}
+
+func strPtr(s string) *string {
+    return &s
+}
+
+func boolPtr(b bool) *bool {
+    return &b
+}
+
+func int32Ptr(i int32) *int32 {
+    return &i
+}
diff --git a/pkg/kafkacomm/handler/pdp_message_handler.go b/pkg/kafkacomm/handler/pdp_message_handler.go
new file mode 100644 (file)
index 0000000..8d7da92
--- /dev/null
@@ -0,0 +1,132 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// The handler package is responsible for processing messages from Kafka, specifically targeting the OPA
+// (Open Policy Agent) PDP (Policy Decision Point). It validates the message type,
+//
+//     ensures it is relevant to the current PDP, and dispatches the message for appropriate processing.
+package handler
+
+import (
+       "encoding/json"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/kafkacomm"
+       "policy-opa-pdp/pkg/kafkacomm/publisher"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/pdpattributes"
+)
+
+type OpaPdpMessage struct {
+       Name        string `json:"name"`        // Name of the PDP (optional for broadcast messages).
+       MessageType string `json:"MessageName"` // Type of the message (e.g., PDP_UPDATE, PDP_STATE_CHANGE, etc.)
+       PdpGroup    string `json:"pdpGroup"`    // Group to which the PDP belongs.
+       PdpSubgroup string `json:"pdpSubgroup"` // Subgroup within the PDP group.
+}
+
+// Checks if the incoming Kafka message belongs to the current PDP instance.
+func checkIfMessageIsForOpaPdp(message OpaPdpMessage) bool {
+
+       if message.Name != "" {
+               // message included a PDP name, check if matches
+               //log.Infof(" Message Name is not empty")
+               return message.Name == pdpattributes.PdpName
+       }
+
+       // message does not provide a PDP name - must be a broadcast
+       if message.PdpGroup == "" {
+               //log.Infof(" Message PDP Group is empty")
+               return false
+       }
+
+       if pdpattributes.PdpSubgroup == "" {
+               // this PDP has no assignment yet, thus should ignore broadcast messages
+               //log.Infof(" pdpstate PDP subgroup is empty")
+               return false
+       }
+
+       if message.PdpGroup != consts.PdpGroup {
+               //log.Infof(" message pdp group is not equal to cons pdp group")
+               return false
+       }
+
+       if message.PdpSubgroup == "" {
+               //message was broadcast to entire group
+               //log.Infof(" message pdp subgroup is empty")
+               return true
+       }
+
+       return message.PdpSubgroup == pdpattributes.PdpSubgroup
+}
+
+// Handles incoming Kafka messages, validates their relevance to the current PDP,
+// and dispatches them for further processing based on their type.
+func PdpMessageHandler(kc *kafkacomm.KafkaConsumer, topic string, p publisher.PdpStatusSender) error {
+
+       log.Debug("Starting PDP Message Listener.....")
+       var stopConsuming bool
+       for !stopConsuming {
+               message, err := kafkacomm.ReadKafkaMessages(kc)
+               if err != nil {
+                       log.Warnf("Failed to Read Kafka Messages: %v\n", err)
+                       continue
+               }
+               log.Debugf("[IN|KAFKA|%s]\n%s", topic, string(message))
+
+               if message != nil {
+
+                       var opaPdpMessage OpaPdpMessage
+
+                       err = json.Unmarshal(message, &opaPdpMessage)
+                       if err != nil {
+                               log.Warnf("Failed to UnMarshal Messages: %v\n", err)
+                               continue
+                       }
+
+                       if !checkIfMessageIsForOpaPdp(opaPdpMessage) {
+
+                               log.Warnf("Not a valid Opa Pdp Message")
+                               continue
+                       }
+
+                       switch opaPdpMessage.MessageType {
+
+                       case "PDP_UPDATE":
+                               err = PdpUpdateMessageHandler(message, p)
+                               if err != nil {
+                                       log.Warnf("Error processing Update Message: %v", err)
+                               }
+
+                       case "PDP_STATE_CHANGE":
+                               err = PdpStateChangeMessageHandler(message, p)
+                               if err != nil {
+                                       log.Warnf("Error processing Update Message: %v", err)
+                               }
+
+                       case "PDP_STATUS":
+                               log.Debugf("discarding event of type PDP_STATUS")
+                               continue
+                       default:
+                               log.Errorf("This is not a valid Message Type: %s", opaPdpMessage.MessageType)
+                               continue
+
+                       }
+
+               }
+       }
+       return nil
+
+}
diff --git a/pkg/kafkacomm/handler/pdp_message_handler_test.go b/pkg/kafkacomm/handler/pdp_message_handler_test.go
new file mode 100644 (file)
index 0000000..3764c9e
--- /dev/null
@@ -0,0 +1,142 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package handler
+
+import (
+       "github.com/stretchr/testify/assert"
+       "policy-opa-pdp/pkg/pdpattributes"
+       "testing"
+)
+
+/*
+checkIfMessageIsForOpaPdp_Check
+Description: Validating Message Attributes
+Input: PDP message
+Expected Output: Returning true stating all the values are validated successfully
+*/
+func TestCheckIfMessageIsForOpaPdp_Check(t *testing.T) {
+
+       var opapdpMessage OpaPdpMessage
+
+       opapdpMessage.Name = "opa-3a318049-813f-4172-b4d3-7d4f466e5b80"
+       opapdpMessage.MessageType = "PDP_STATUS"
+       opapdpMessage.PdpGroup = "defaultGroup"
+       opapdpMessage.PdpSubgroup = "opa"
+
+       assert.False(t, checkIfMessageIsForOpaPdp(opapdpMessage), "Its a valid Opa Pdp Message")
+
+}
+
+/*
+checkIfMessageIsForOpaPdp_Check_Message_Name
+Description: Validating Message Attributes
+Input: PDP message with name as empty
+Expected Output: Returning Error since it is not valid message
+*/
+func TestCheckIfMessageIsForOpaPdp_Check_Message_Name(t *testing.T) {
+
+       var opapdpMessage OpaPdpMessage
+
+       opapdpMessage.Name = ""
+       opapdpMessage.MessageType = "PDP_STATUS"
+       opapdpMessage.PdpGroup = "defaultGroup"
+       opapdpMessage.PdpSubgroup = "opa"
+
+       assert.False(t, checkIfMessageIsForOpaPdp(opapdpMessage), "Not a valid Opa Pdp Message")
+
+}
+
+/*
+checkIfMessageIsForOpaPdp_Check_PdpGroup
+Description: Validating Message Attributes
+Input: PDP message with invalid PdpGroup
+Expected Output: Returning Error since it is not valid message
+*/
+func TestCheckIfMessageIsForOpaPdp_Check_PdpGroup(t *testing.T) {
+
+       var opapdpMessage OpaPdpMessage
+
+       opapdpMessage.Name = ""
+       opapdpMessage.MessageType = "PDP_STATUS"
+       opapdpMessage.PdpGroup = "defaultGroup"
+       opapdpMessage.PdpSubgroup = "opa"
+
+       pdpattributes.PdpSubgroup = "opa"
+       assert.True(t, checkIfMessageIsForOpaPdp(opapdpMessage), "Its a valid Opa Pdp Message")
+
+}
+
+/*
+checkIfMessageIsForOpaPdp_Check_EmptyPdpGroup
+Description: Validating Message Attributes
+Input: PDP Group Empty
+Expected Output: Returning Error since it is not valid message
+*/
+func TestCheckIfMessageIsForOpaPdp_Check_EmptyPdpGroup(t *testing.T) {
+
+       var opapdpMessage OpaPdpMessage
+
+       opapdpMessage.Name = ""
+       opapdpMessage.MessageType = "PDP_STATUS"
+       opapdpMessage.PdpGroup = ""
+       opapdpMessage.PdpSubgroup = "opa"
+
+       assert.False(t, checkIfMessageIsForOpaPdp(opapdpMessage), "Not a valid Opa Pdp Message")
+
+}
+
+/*
+checkIfMessageIsForOpaPdp_Check_PdpSubgroup
+Description: Validating Message Attributes
+Input: PDP message with invalid PdpSubgroup
+Expected Output: Returning Error since it is not valid message
+*/
+func TestCheckIfMessageIsForOpaPdp_Check_PdpSubgroup(t *testing.T) {
+
+       var opapdpMessage OpaPdpMessage
+
+       opapdpMessage.Name = ""
+       opapdpMessage.MessageType = "PDP_STATUS"
+       opapdpMessage.PdpGroup = "defaultGroup"
+       opapdpMessage.PdpSubgroup = "opa"
+
+       pdpattributes.PdpSubgroup = "opa"
+       assert.True(t, checkIfMessageIsForOpaPdp(opapdpMessage), "It's a valid Opa Pdp Message")
+
+}
+
+/*
+checkIfMessageIsForOpaPdp_Check_IncorrectPdpSubgroup
+Description: Validating Message Attributes
+Input: PDP message with empty  PdpSubgroup
+Expected Output: Returning Error since it is not valid message
+*/
+func TestCheckIfMessageIsForOpaPdp_Check_IncorrectPdpSubgroup(t *testing.T) {
+
+       var opapdpMessage OpaPdpMessage
+
+       opapdpMessage.Name = ""
+       opapdpMessage.MessageType = "PDP_STATUS"
+       opapdpMessage.PdpGroup = "defaultGroup"
+       opapdpMessage.PdpSubgroup = "o"
+
+       pdpattributes.PdpSubgroup = "opa"
+       assert.False(t, checkIfMessageIsForOpaPdp(opapdpMessage), "Not a valid Opa Pdp Message")
+
+}
diff --git a/pkg/kafkacomm/handler/pdp_state_change_handler.go b/pkg/kafkacomm/handler/pdp_state_change_handler.go
new file mode 100644 (file)
index 0000000..32d998f
--- /dev/null
@@ -0,0 +1,57 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// will process the state change message from pap and send the pdp status response.
+package handler
+
+import (
+       "encoding/json"
+       "policy-opa-pdp/pkg/kafkacomm/publisher"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/model"
+       "policy-opa-pdp/pkg/pdpstate"
+)
+
+// Processes incoming messages indicating a PDP state change.
+// This includes updating the PDP state and sending a status response when the state transitions.
+func PdpStateChangeMessageHandler(message []byte, p publisher.PdpStatusSender) error {
+
+       var pdpStateChange model.PdpStateChange
+
+       err := json.Unmarshal(message, &pdpStateChange)
+       if err != nil {
+               log.Debugf("Failed to UnMarshal Messages: %v\n", err)
+               return err
+       }
+
+       log.Debugf("PDP STATE CHANGE message received: %s", string(message))
+
+       if pdpStateChange.State != "" {
+               pdpstate.SetState(pdpStateChange.State)
+
+       }
+
+       log.Debugf("State change from PASSIVE To : %s", pdpstate.GetState())
+       err = publisher.SendStateChangeResponse(p, &pdpStateChange)
+       if err != nil {
+               log.Debugf("Failed to Send State Change Response Message: %v\n", err)
+               return err
+       }
+       log.Infof("PDP_STATUS With State Change Message Sent Successfully")
+
+       return nil
+}
diff --git a/pkg/kafkacomm/handler/pdp_state_change_handler_test.go b/pkg/kafkacomm/handler/pdp_state_change_handler_test.go
new file mode 100644 (file)
index 0000000..f7e8f84
--- /dev/null
@@ -0,0 +1,93 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package handler
+
+import (
+       "policy-opa-pdp/pkg/kafkacomm/publisher"
+       "policy-opa-pdp/pkg/model"
+       "policy-opa-pdp/pkg/pdpstate"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+)
+
+// MockPdpStatusSender is a mock implementation of the PdpStatusSender interface
+type MockPdpStatusSender struct {
+       mock.Mock
+}
+
+func (m *MockPdpStatusSender) SendStateChangeResponse(p *publisher.PdpStatusSender, pdpStateChange *model.PdpStateChange) error {
+       args := m.Called(p, pdpStateChange)
+       return args.Error(0)
+}
+
+func (m *MockPdpStatusSender) SendPdpStatus(status model.PdpStatus) error {
+       args := m.Called(status)
+       return args.Error(0)
+}
+
+func TestPdpStateChangeMessageHandler(t *testing.T) {
+
+       // Create a mock PdpStatusSender
+       mockSender := new(MockPdpStatusSender)
+
+       // Define test cases
+       tests := []struct {
+               name          string
+               message       []byte
+               expectedState string
+               mockError     error
+               expectError   bool
+       }{
+               {
+                       name:          "Valid state change",
+                       message:       []byte(`{"state":"ACTIVE"}`),
+                       expectedState: "ACTIVE",
+                       mockError:     nil,
+                       expectError:   false,
+               },
+               {
+                       name:        "Invalid JSON",
+                       message:     []byte(`{"state":}`),
+                       mockError:   nil,
+                       expectError: true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       // Set up the mock to return the expected error
+                       mockSender.On("SendStateChangeResponse", mock.Anything, mock.Anything).Return(tt.mockError)
+                       mockSender.On("SendPdpStatus", mock.Anything).Return(nil)
+
+                       // Call the handler
+                       err := PdpStateChangeMessageHandler(tt.message, mockSender)
+
+                       // Check the results
+                       if tt.expectError {
+                               assert.Error(t, err)
+                       } else {
+                               assert.NoError(t, err)
+                               assert.Equal(t, tt.expectedState, pdpstate.GetState().String())
+                       }
+
+               })
+       }
+}
diff --git a/pkg/kafkacomm/handler/pdp_update_message_handler.go b/pkg/kafkacomm/handler/pdp_update_message_handler.go
new file mode 100644 (file)
index 0000000..632bcc8
--- /dev/null
@@ -0,0 +1,64 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// will process the update message from pap and send the pdp status response.
+package handler
+
+import (
+       "encoding/json"
+       "github.com/go-playground/validator/v10"
+       "policy-opa-pdp/pkg/kafkacomm/publisher"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/model"
+       "policy-opa-pdp/pkg/pdpattributes"
+)
+
+// Handles messages of type PDP_UPDATE sent from the Policy Administration Point (PAP).
+// It validates the incoming data, updates PDP attributes, and sends a response back to the sender.
+func PdpUpdateMessageHandler(message []byte, p publisher.PdpStatusSender) error {
+
+       var pdpUpdate model.PdpUpdate
+       err := json.Unmarshal(message, &pdpUpdate)
+       if err != nil {
+               log.Debugf("Failed to UnMarshal Messages: %v\n", err)
+               return err
+       }
+       //Initialize Validator and validate Struct after unmarshalling
+       validate := validator.New()
+
+       err = validate.Struct(pdpUpdate)
+       if err != nil {
+               for _, err := range err.(validator.ValidationErrors) {
+                       log.Infof("Field %s failed on the %s tag\n", err.Field(), err.Tag())
+               }
+               return err
+       }
+
+       log.Debugf("PDP_UPDATE Message received: %s", string(message))
+
+       pdpattributes.SetPdpSubgroup(pdpUpdate.PdpSubgroup)
+       pdpattributes.SetPdpHeartbeatInterval(pdpUpdate.PdpHeartbeatIntervalMs)
+
+       err = publisher.SendPdpUpdateResponse(p, &pdpUpdate)
+       if err != nil {
+               log.Debugf("Failed to Send Update Response Message: %v\n", err)
+               return err
+       }
+       log.Infof("PDP_STATUS Message Sent Successfully")
+       go publisher.StartHeartbeatIntervalTimer(pdpattributes.PdpHeartbeatInterval, p)
+       return nil
+}
diff --git a/pkg/kafkacomm/handler/pdp_update_message_handler_test.go b/pkg/kafkacomm/handler/pdp_update_message_handler_test.go
new file mode 100644 (file)
index 0000000..061f1ce
--- /dev/null
@@ -0,0 +1,196 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package handler
+
+import (
+       "errors"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+       "policy-opa-pdp/pkg/kafkacomm/publisher/mocks"
+       "testing"
+)
+
+/*
+PdpUpdateMessageHandler_success
+Description: Test by sending a valid input message for pdp update
+Input: valid input
+Expected Output: PDP Update Message should be sent sucessfully.
+*/
+func TestPdpUpdateMessageHandler_Success(t *testing.T) {
+
+       messageString := `{
+               "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0",
+               "pdpHeartbeatIntervalMs":120000,
+               "policiesToBeDeployed":[],
+               "policiesToBeUndeployed":[],
+               "messageName":"PDP_UPDATE",
+               "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1",
+               "timestampMs":1730722305297,
+               "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059",
+               "pdpGroup":"defaultGroup",
+               "pdpSubgroup":"opa"
+                }`
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(nil)
+
+       err := PdpUpdateMessageHandler([]byte(messageString), mockSender)
+       assert.NoError(t, err)
+
+}
+
+/*
+PdpUpdateMessageHandler_Message_Unmarshal_Failure1
+Description: Test by sending a invalid input message which should result in a Json unmarhsal error
+Input: invalid input Message by renaming params or removing certain params
+Expected Output: Message Handler should exit gracefully stating the error.
+*/
+func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure1(t *testing.T) {
+
+       // sending only source parameter in the message string
+       messageString := `{
+               "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0"}`
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Jsonunmarshal Error"))
+
+       err := PdpUpdateMessageHandler([]byte(messageString), mockSender)
+       assert.Error(t, err)
+
+}
+
+/*
+PdpUpdateMessageHandler_Message_Unmarshal_Failure2
+Description: Test by sending a invalid input message which should result in a Json unmarhsal error
+Input: invalid input Message by renaming params or removing certain params
+Expected Output: Message Handler should exit gracefully stating the error.
+*/
+func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure2(t *testing.T) {
+
+       // invlaid params by mispelling a param  "source"
+
+       messageString := `{
+               "soce":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0",
+               "pdpHeartbeatIntervalMs":120000}`
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Jsonunmarshal Error"))
+
+       err := PdpUpdateMessageHandler([]byte(messageString), mockSender)
+       assert.Error(t, err)
+
+}
+
+/*
+PdpUpdateMessageHandler_Message_Unmarshal_Failure3
+Description: Test by sending a invalid input message which should result in a Json unmarhsal error
+Input: {}
+Expected Output: Message Handler should exit gracefully stating the error.
+*/
+func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure3(t *testing.T) {
+
+       // invlaid params by mispelling a param  "source"
+
+       messageString := `{
+                "soce:"pap-c17b4dbc-3278-483a-ace9-98f3157245c0",
+                "pdpHeartbeatIntervalMs":120000}`
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Jsonunmarshal Error"))
+
+       err := PdpUpdateMessageHandler([]byte(messageString), mockSender)
+       assert.Error(t, err)
+
+}
+
+/*
+PdpUpdateMessageHandler_Message_Unmarshal_Failure4
+Description: Test by sending a invalid input message which should result in a Json unmarhsal error
+Input: empty
+Expected Output: Message Handler should exit gracefully stating the error.
+*/
+func TestPdpUpdateMessageHandler_Message_Unmarshal_Failure4(t *testing.T) {
+
+       // invlaid params by mispelling a param  "source"
+
+       messageString := `""`
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Jsonunmarshal Error"))
+
+       err := PdpUpdateMessageHandler([]byte(messageString), mockSender)
+       assert.Error(t, err)
+
+}
+
+/*
+PdpUpdateMessageHandler_Fails_Sending_PdpUpdateResponse
+Description: Test by sending a invalid attribute for pdpstate which should result in a failure in sending pdp update response
+Input: invalid input config set for pdpstate
+Expected Output: Message Handler should exit gracefully stating the error.
+*/
+func TestPdpUpdateMessageHandler_Fails_Sending_UpdateResponse(t *testing.T) {
+
+       // invalid value set to pdpSubgroup -->empty ""
+       messageString := `{
+               "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0",
+               "pdpHeartbeatIntervalMs":120000,
+               "policiesToBeDeployed":[],
+               "policiesToBeUndeployed":[],
+               "messageName":"PDP_UPDATE",
+               "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1",
+               "timestampMs":1730722305297,
+               "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059",
+               "pdpGroup":"defaultGroup"
+                }`
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Error in Sending PDP Update Response"))
+
+       err := PdpUpdateMessageHandler([]byte(messageString), mockSender)
+       assert.Error(t, err)
+
+}
+
+/*
+PdpUpdateMessageHandler_Invalid_Starttimeinterval
+Description: Test by sending a invalid time value attribute for pdpstate which should result in a failure in starting heartbeat interval
+Input: invalid input message for pdpstate heartbeat interval
+Expected Output: Message Handler should exit gracefully stating the error.
+*/
+func TestPdpUpdateMessageHandler_Invalid_Starttimeinterval(t *testing.T) {
+
+       //invalid interval set to negative -1000
+       messageString := `{
+               "source":"pap-c17b4dbc-3278-483a-ace9-98f3157245c0",
+               "pdpHeartbeatIntervalMs":-1000,
+               "policiesToBeDeployed":[],
+               "policiesToBeUndeployed":[],
+               "messageName":"PDP_UPDATE",
+               "requestId":"41c117db-49a0-40b0-8586-5580d042d0a1",
+               "timestampMs":1730722305297,
+               "name":"opa-21cabb3e-f652-4ca6-b498-a77e62fcd059",
+               "pdpGroup":"defaultGroup",
+               "pdpSubgroup":"opa"
+                }`
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Invalid Interval Time for Heartbeat"))
+
+       err := PdpUpdateMessageHandler([]byte(messageString), mockSender)
+       assert.Error(t, err)
+
+}
diff --git a/pkg/kafkacomm/mocks/kafkaconsumerinterface.go b/pkg/kafkacomm/mocks/kafkaconsumerinterface.go
new file mode 100644 (file)
index 0000000..ca5140e
--- /dev/null
@@ -0,0 +1,96 @@
+// Code generated by mockery v2.46.3. DO NOT EDIT.
+
+package mocks
+
+import (
+       kafka "github.com/confluentinc/confluent-kafka-go/kafka"
+
+       mock "github.com/stretchr/testify/mock"
+
+       time "time"
+)
+
+// KafkaConsumerInterface is an autogenerated mock type for the KafkaConsumerInterface type
+type KafkaConsumerInterface struct {
+       mock.Mock
+}
+
+// Close provides a mock function with given fields:
+func (_m *KafkaConsumerInterface) Close() error {
+       ret := _m.Called()
+
+       if len(ret) == 0 {
+               panic("no return value specified for Close")
+       }
+
+       var r0 error
+       if rf, ok := ret.Get(0).(func() error); ok {
+               r0 = rf()
+       } else {
+               r0 = ret.Error(0)
+       }
+
+       return r0
+}
+
+// ReadMessage provides a mock function with given fields: timeout
+func (_m *KafkaConsumerInterface) ReadMessage(timeout time.Duration) (*kafka.Message, error) {
+       ret := _m.Called(timeout)
+
+       if len(ret) == 0 {
+               panic("no return value specified for ReadMessage")
+       }
+
+       var r0 *kafka.Message
+       var r1 error
+       if rf, ok := ret.Get(0).(func(time.Duration) (*kafka.Message, error)); ok {
+               return rf(timeout)
+       }
+       if rf, ok := ret.Get(0).(func(time.Duration) *kafka.Message); ok {
+               r0 = rf(timeout)
+       } else {
+               if ret.Get(0) != nil {
+                       r0 = ret.Get(0).(*kafka.Message)
+               }
+       }
+
+       if rf, ok := ret.Get(1).(func(time.Duration) error); ok {
+               r1 = rf(timeout)
+       } else {
+               r1 = ret.Error(1)
+       }
+
+       return r0, r1
+}
+
+// Unsubscribe provides a mock function with given fields:
+func (_m *KafkaConsumerInterface) Unsubscribe() error {
+       ret := _m.Called()
+
+       if len(ret) == 0 {
+               panic("no return value specified for Unsubscribe")
+       }
+
+       var r0 error
+       if rf, ok := ret.Get(0).(func() error); ok {
+               r0 = rf()
+       } else {
+               r0 = ret.Error(0)
+       }
+
+       return r0
+}
+
+// NewKafkaConsumerInterface creates a new instance of KafkaConsumerInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewKafkaConsumerInterface(t interface {
+       mock.TestingT
+       Cleanup(func())
+}) *KafkaConsumerInterface {
+       mock := &KafkaConsumerInterface{}
+       mock.Mock.Test(t)
+
+       t.Cleanup(func() { mock.AssertExpectations(t) })
+
+       return mock
+}
diff --git a/pkg/kafkacomm/mocks/kafkaproducerinterface.go b/pkg/kafkacomm/mocks/kafkaproducerinterface.go
new file mode 100644 (file)
index 0000000..97b6f53
--- /dev/null
@@ -0,0 +1,51 @@
+// Code generated by mockery v2.46.3. DO NOT EDIT.
+
+package mocks
+
+import (
+       kafka "github.com/confluentinc/confluent-kafka-go/kafka"
+
+       mock "github.com/stretchr/testify/mock"
+)
+
+// KafkaProducerInterface is an autogenerated mock type for the KafkaProducerInterface type
+type KafkaProducerInterface struct {
+       mock.Mock
+}
+
+// Close provides a mock function with given fields:
+func (_m *KafkaProducerInterface) Close() {
+       _m.Called()
+}
+
+// Produce provides a mock function with given fields: _a0, _a1
+func (_m *KafkaProducerInterface) Produce(_a0 *kafka.Message, _a1 chan kafka.Event) error {
+       ret := _m.Called(_a0, _a1)
+
+       if len(ret) == 0 {
+               panic("no return value specified for Produce")
+       }
+
+       var r0 error
+       if rf, ok := ret.Get(0).(func(*kafka.Message, chan kafka.Event) error); ok {
+               r0 = rf(_a0, _a1)
+       } else {
+               r0 = ret.Error(0)
+       }
+
+       return r0
+}
+
+// NewKafkaProducerInterface creates a new instance of KafkaProducerInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewKafkaProducerInterface(t interface {
+       mock.TestingT
+       Cleanup(func())
+}) *KafkaProducerInterface {
+       mock := &KafkaProducerInterface{}
+       mock.Mock.Test(t)
+
+       t.Cleanup(func() { mock.AssertExpectations(t) })
+
+       return mock
+}
diff --git a/pkg/kafkacomm/pdp_topic_consumer.go b/pkg/kafkacomm/pdp_topic_consumer.go
new file mode 100644 (file)
index 0000000..4858bdf
--- /dev/null
@@ -0,0 +1,103 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// kafkacomm package provides a structured way to create and manage Kafka consumers,
+// handle subscriptions, and read messages from Kafka topics
+package kafkacomm
+
+import (
+       "github.com/confluentinc/confluent-kafka-go/kafka"
+       "policy-opa-pdp/cfg"
+       "policy-opa-pdp/pkg/log"
+       "time"
+)
+
+// KafkaConsumerInterface defines the interface for a Kafka consumer.
+type KafkaConsumerInterface interface {
+       Close() error
+       Unsubscribe() error
+       ReadMessage(timeout time.Duration) (*kafka.Message, error)
+}
+
+// KafkaConsumer is a wrapper around the Kafka consumer.
+type KafkaConsumer struct {
+       Consumer KafkaConsumerInterface
+}
+
+// Close closes the KafkaConsumer
+func (kc *KafkaConsumer) Close() {
+       kc.Consumer.Close()
+}
+
+// Unsubscribe unsubscribes the KafkaConsumer
+func (kc *KafkaConsumer) Unsubscribe() error {
+       if err := kc.Consumer.Unsubscribe(); err != nil {
+               log.Warnf("Error Unsubscribing :%v", err)
+               return err
+       }
+       log.Debug("Unsubscribe From Topic")
+       return nil
+}
+
+// creates a new Kafka consumer and returns it
+func NewKafkaConsumer() (*KafkaConsumer, error) {
+       brokers := cfg.BootstrapServer
+       groupid := cfg.GroupId
+       topic := cfg.Topic
+       useSASL := cfg.UseSASLForKAFKA
+       username := cfg.KAFKA_USERNAME
+       password := cfg.KAFKA_PASSWORD
+
+       // Add Kafka Connection Properties ....
+       configMap := &kafka.ConfigMap{
+               "bootstrap.servers": brokers,
+               "group.id":          groupid,
+               "auto.offset.reset": "earliest",
+       }
+       //for STRIMZI-KAFKA in case sasl is enabled
+       if useSASL == "true" {
+               configMap.SetKey("sasl.mechanism", "SCRAM-SHA-512")
+               configMap.SetKey("sasl.username", username)
+               configMap.SetKey("sasl.password", password)
+               configMap.SetKey("security.protocol", "SASL_PLAINTEXT")
+       }
+
+       // create new Kafka Consumer
+       consumer, err := kafka.NewConsumer(configMap)
+       if err != nil {
+               log.Warnf("Error creating consumer: %v\n", err)
+               return nil, err
+       }
+       //subscribe to topic
+       err = consumer.SubscribeTopics([]string{topic}, nil)
+       if err != nil {
+               log.Warnf("Error subcribing to topic: %v\n", err)
+               return nil, err
+       }
+       log.Debugf("Topic Subscribed... : %v", topic)
+       return &KafkaConsumer{Consumer: consumer}, nil
+}
+
+// gets the Kafka messages on the subscribed topic
+func ReadKafkaMessages(kc *KafkaConsumer) ([]byte, error) {
+       msg, err := kc.Consumer.ReadMessage(-1)
+       if err != nil {
+               log.Warnf("Error reading Kafka message: %v", err)
+               return nil, err
+       }
+       return msg.Value, nil
+}
diff --git a/pkg/kafkacomm/pdp_topic_consumer_test.go b/pkg/kafkacomm/pdp_topic_consumer_test.go
new file mode 100644 (file)
index 0000000..2fdfa90
--- /dev/null
@@ -0,0 +1,129 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package kafkacomm
+
+import (
+       "errors"
+       "policy-opa-pdp/pkg/kafkacomm/mocks"
+       "testing"
+
+       "github.com/confluentinc/confluent-kafka-go/kafka"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+)
+
+func TestNewKafkaConsumer(t *testing.T) {
+       // Assuming configuration is correctly loaded from cfg package
+       // You can mock or override cfg values here if needed
+
+       consumer, err := NewKafkaConsumer()
+       assert.NoError(t, err, "Expected no error when creating Kafka consumer")
+       assert.NotNil(t, consumer, "Expected a non-nil KafkaConsumer")
+
+       // Clean up
+       if consumer != nil {
+               consumer.Close()
+       }
+}
+
+func TestReadKafkaMessages_Success(t *testing.T) {
+       // Create a new mock for ConsumerInterface
+       mockConsumer := new(mocks.KafkaConsumerInterface)
+
+       // Create a KafkaConsumer with the mock
+       kc := &KafkaConsumer{Consumer: mockConsumer}
+
+       // Define the expected message
+       expectedMsg := &kafka.Message{Value: []byte("test message")}
+
+       // Set up the mock to return the expected message
+       mockConsumer.On("ReadMessage", mock.Anything).Return(expectedMsg, nil)
+
+       // Test ReadKafkaMessages
+       msg, err := ReadKafkaMessages(kc)
+       assert.NoError(t, err, "Expected no error when reading message")
+       assert.Equal(t, expectedMsg.Value, msg, "Expected message content to match")
+
+       // Assert expectations
+       mockConsumer.AssertExpectations(t)
+}
+
+func TestReadKafkaMessages_Error(t *testing.T) {
+       mockConsumer := new(mocks.KafkaConsumerInterface)
+
+       kc := &KafkaConsumer{Consumer: mockConsumer}
+
+       // Set up the mock to return an error
+       expectedErr := errors.New("read error")
+       mockConsumer.On("ReadMessage", mock.Anything).Return(nil, expectedErr)
+
+       msg, err := ReadKafkaMessages(kc)
+       assert.Error(t, err, "Expected an error when reading message")
+       assert.Nil(t, msg, "Expected message to be nil on error")
+
+       mockConsumer.AssertExpectations(t)
+}
+
+func TestKafkaConsumer_Close(t *testing.T) {
+       mockConsumer := new(mocks.KafkaConsumerInterface)
+
+       kc := &KafkaConsumer{Consumer: mockConsumer}
+
+       // Set up the mock for Close
+       mockConsumer.On("Close").Return(nil)
+
+       // Test Close method
+       kc.Close()
+
+       // Verify that Close was called
+       mockConsumer.AssertExpectations(t)
+}
+
+func TestKafkaConsumer_Unsubscribe(t *testing.T) {
+       mockConsumer := new(mocks.KafkaConsumerInterface)
+
+       kc := &KafkaConsumer{Consumer: mockConsumer}
+
+       // Set up the mock for Unsubscribe
+       mockConsumer.On("Unsubscribe").Return(nil)
+
+       // Test Unsubscribe method
+       err := kc.Unsubscribe()
+       assert.NoError(t, err)
+
+       // Verify that Unsubscribe was called
+       mockConsumer.AssertExpectations(t)
+}
+
+func TestKafkaConsumer_Unsubscribe_Error(t *testing.T) {
+       mockConsumer := new(mocks.KafkaConsumerInterface)
+       mockError := errors.New("Unsubscribe error")
+       kc := &KafkaConsumer{Consumer: mockConsumer}
+
+       // Set up the mock for Unsubscribe
+       mockConsumer.On("Unsubscribe").Return(mockError)
+
+       // Test Unsubscribe method
+       err := kc.Unsubscribe()
+       assert.Error(t, err)
+       assert.Equal(t, mockError, err)
+
+       // Verify that Unsubscribe was called
+       mockConsumer.AssertExpectations(t)
+}
diff --git a/pkg/kafkacomm/pdp_topic_producer.go b/pkg/kafkacomm/pdp_topic_producer.go
new file mode 100644 (file)
index 0000000..1b11b35
--- /dev/null
@@ -0,0 +1,107 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package kafkacomm provides utilities for producing messages to a Kafka topic
+// using a configurable Kafka producer. It supports SASL authentication and
+// dynamic topic configuration.
+package kafkacomm
+
+import (
+       "github.com/confluentinc/confluent-kafka-go/kafka"
+       "policy-opa-pdp/cfg"
+       "sync"
+       "log"
+)
+
+type KafkaProducerInterface interface {
+       Produce(*kafka.Message, chan kafka.Event) error
+       Close()
+}
+
+// KafkaProducer wraps a Kafka producer instance and a topic to provide
+// a simple interface for producing messages.
+type KafkaProducer struct {
+       producer KafkaProducerInterface
+       topic    string
+}
+
+var (
+       instance *KafkaProducer
+       once     sync.Once
+)
+
+// GetKafkaProducer initializes and returns a KafkaProducer instance which is a singleton.
+// It configures the Kafka producer with the given bootstrap servers and topic.
+// If SASL authentication is enabled via the configuration, the necessary credentials
+// are set in the producer configuration.
+func GetKafkaProducer(bootstrapServers, topic string) (*KafkaProducer, error) {
+       var err error
+       once.Do(func() {
+               brokers := cfg.BootstrapServer
+               useSASL := cfg.UseSASLForKAFKA
+               username := cfg.KAFKA_USERNAME
+               password := cfg.KAFKA_PASSWORD
+
+               // Add Kafka Connection Properties ....
+               configMap := &kafka.ConfigMap{
+                       "bootstrap.servers": brokers,
+               }
+
+               if useSASL == "true" {
+                       configMap.SetKey("sasl.mechanism", "SCRAM-SHA-512")
+                       configMap.SetKey("sasl.username", username)
+                       configMap.SetKey("sasl.password", password)
+                       configMap.SetKey("security.protocol", "SASL_PLAINTEXT")
+               }
+
+               p, err := kafka.NewProducer(configMap)
+               if err != nil {
+                       return
+               }
+               instance = &KafkaProducer{
+                       producer: p,
+                       topic:    topic,
+               }
+
+       })
+       return instance, err
+}
+
+// Produce sends a message to the configured Kafka topic.
+// It takes the message payload as a byte slice and returns any errors
+func (kp *KafkaProducer) Produce(message []byte) error {
+       kafkaMessage := &kafka.Message{
+               TopicPartition: kafka.TopicPartition{Topic: &kp.topic, Partition: kafka.PartitionAny},
+               Value:          []byte(message),
+       }
+       err := kp.producer.Produce(kafkaMessage, nil)
+       if err != nil {
+               return err
+       }
+       return nil
+}
+
+// Close shuts down the Kafka producer, releasing all resources.
+func (kp *KafkaProducer) Close() {
+
+       if kp == nil || kp.producer == nil {
+               log.Println("KafkaProducer or producer is nil, skipping Close.")
+               return
+       }
+       kp.producer.Close()
+       log.Println("KafkaProducer closed successfully.")
+}
diff --git a/pkg/kafkacomm/pdp_topic_producer_test.go b/pkg/kafkacomm/pdp_topic_producer_test.go
new file mode 100644 (file)
index 0000000..55f3bc8
--- /dev/null
@@ -0,0 +1,117 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package kafkacomm
+
+import (
+       "errors"
+       "testing"
+       "time"
+       //      "github.com/confluentinc/confluent-kafka-go/kafka"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+
+       "policy-opa-pdp/pkg/kafkacomm/mocks" // Adjust to your actual mock path
+)
+
+func TestKafkaProducer_Produce_Success(t *testing.T) {
+       done := make(chan struct{})
+
+       go func() {
+               defer close(done)
+               // Arrange
+               mockProducer := new(mocks.KafkaProducerInterface)
+               topic := "test-topic"
+               kp := &KafkaProducer{
+                       producer: mockProducer,
+                       topic:    topic,
+               }
+
+               message := []byte("test message")
+
+               // Mock Produce method to simulate successful delivery
+               mockProducer.On("Produce", mock.Anything, mock.Anything).Return(nil)
+
+               // Act
+               err := kp.Produce(message)
+
+               assert.NoError(t, err)
+               mockProducer.AssertExpectations(t)
+       }()
+       select {
+       case <-done:
+       case <-time.After(10 * time.Second):
+               t.Fatal("test timed out")
+       }
+
+}
+
+func TestKafkaProducer_Produce_Error(t *testing.T) {
+       // Arrange
+       mockProducer := new(mocks.KafkaProducerInterface)
+       topic := "test-topic"
+       kp := &KafkaProducer{
+               producer: mockProducer,
+               topic:    topic,
+       }
+
+       // Simulate production error
+       mockProducer.On("Produce", mock.Anything, mock.Anything).Return(errors.New("produce error"))
+
+       // Act
+       err := kp.Produce([]byte("test message"))
+
+       // Assert
+       assert.Error(t, err)
+       assert.Equal(t, "produce error", err.Error())
+       mockProducer.AssertExpectations(t)
+}
+
+func TestKafkaProducer_Close(t *testing.T) {
+       // Arrange
+       mockProducer := new(mocks.KafkaProducerInterface)
+       kp := &KafkaProducer{
+               producer: mockProducer,
+       }
+
+       // Simulate successful close
+       mockProducer.On("Close").Return()
+
+       // Act
+       kp.Close()
+
+       // Assert
+       mockProducer.AssertExpectations(t)
+}
+
+func TestKafkaProducer_Close_Error(t *testing.T) {
+       // Arrange
+       mockProducer := new(mocks.KafkaProducerInterface)
+       kp := &KafkaProducer{
+               producer: mockProducer,
+       }
+
+       // Simulate close error
+       mockProducer.On("Close").Return()
+
+       // Act
+       kp.Close()
+
+       // Assert
+       mockProducer.AssertExpectations(t)
+}
diff --git a/pkg/kafkacomm/publisher/mocks/PdpStatusSender.go b/pkg/kafkacomm/publisher/mocks/PdpStatusSender.go
new file mode 100644 (file)
index 0000000..f9cc279
--- /dev/null
@@ -0,0 +1,46 @@
+// Code generated by mockery v2.46.3. DO NOT EDIT.
+
+package mocks
+
+import (
+       model "policy-opa-pdp/pkg/model"
+
+       mock "github.com/stretchr/testify/mock"
+)
+
+// PdpStatusSender is an autogenerated mock type for the PdpStatusSender type
+type PdpStatusSender struct {
+       mock.Mock
+}
+
+// SendPdpStatus provides a mock function with given fields: pdpStatus
+func (_m *PdpStatusSender) SendPdpStatus(pdpStatus model.PdpStatus) error {
+       ret := _m.Called(pdpStatus)
+
+       if len(ret) == 0 {
+               panic("no return value specified for SendPdpStatus")
+       }
+
+       var r0 error
+       if rf, ok := ret.Get(0).(func(model.PdpStatus) error); ok {
+               r0 = rf(pdpStatus)
+       } else {
+               r0 = ret.Error(0)
+       }
+
+       return r0
+}
+
+// NewPdpStatusSender creates a new instance of PdpStatusSender. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewPdpStatusSender(t interface {
+       mock.TestingT
+       Cleanup(func())
+}) *PdpStatusSender {
+       mock := &PdpStatusSender{}
+       mock.Mock.Test(t)
+
+       t.Cleanup(func() { mock.AssertExpectations(t) })
+
+       return mock
+}
diff --git a/pkg/kafkacomm/publisher/pdp-heartbeat.go b/pkg/kafkacomm/publisher/pdp-heartbeat.go
new file mode 100644 (file)
index 0000000..f814992
--- /dev/null
@@ -0,0 +1,111 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// The publisher package is responsible for managing periodic heartbeat messages for the
+// Open Policy Agent (OPA) Policy Decision Point (PDP) and publishing the PDP's status to relevant channels.
+// It provides functions to initialize, manage, and stop timers for sending heartbeat messages,
+// ensuring the PDP communicates its health and state periodically.
+package publisher
+
+import (
+       "fmt"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/model"
+       "policy-opa-pdp/pkg/pdpattributes"
+       "policy-opa-pdp/pkg/pdpstate"
+       "time"
+
+       "github.com/google/uuid"
+)
+
+var (
+       ticker          *time.Ticker
+       stopChan        chan bool
+       currentInterval int64
+)
+
+// Initializes a timer that sends periodic heartbeat messages to indicate the health and state of the PDP.
+func StartHeartbeatIntervalTimer(intervalMs int64, s PdpStatusSender) {
+       if intervalMs <= 0 {
+               log.Errorf("Invalid interval provided: %d. Interval must be greater than zero.", intervalMs)
+               ticker = nil
+               return
+       }
+
+       if ticker != nil && intervalMs == currentInterval {
+               log.Debug("Ticker is already running")
+               return
+       }
+
+       if ticker != nil {
+               ticker.Stop()
+       }
+       // StopTicker()
+       currentInterval = intervalMs
+
+       ticker = time.NewTicker(time.Duration(intervalMs) * time.Millisecond)
+       log.Debugf("New Ticker %d", currentInterval)
+       stopChan = make(chan bool)
+       go func() {
+               for {
+                       select {
+                       case <-ticker.C:
+                               sendPDPHeartBeat(s)
+                       case <-stopChan:
+                               ticker.Stop()
+                               return
+                       }
+               }
+       }()
+}
+
+// Creates and sends a heartbeat message with the PDP's current state, health, and attributes
+func sendPDPHeartBeat(s PdpStatusSender) error {
+       pdpStatus := model.PdpStatus{
+               MessageType: model.PDP_STATUS,
+               PdpType:     consts.PdpType,
+               State:       pdpstate.GetState(),
+               Healthy:     model.Healthy,
+               Name:        pdpattributes.PdpName,
+               Description: "Pdp heartbeat",
+               PdpGroup:    consts.PdpGroup,
+               PdpSubgroup: &pdpattributes.PdpSubgroup,
+       }
+       pdpStatus.RequestID = uuid.New().String()
+       pdpStatus.TimestampMs = fmt.Sprintf("%d", time.Now().UnixMilli())
+
+       err := s.SendPdpStatus(pdpStatus)
+       log.Debugf("Sending Heartbeat ...")
+       if err != nil {
+               log.Warnf("Error producing message: %v\n", err)
+               return err
+       } else {
+               return nil
+       }
+}
+
+// Stops the running ticker and terminates the goroutine managing heartbeat messages.
+func StopTicker() {
+       if ticker != nil && stopChan != nil {
+               stopChan <- true
+               close(stopChan)
+               ticker = nil
+       } else {
+               log.Debugf("Ticker is not Running")
+       }
+}
diff --git a/pkg/kafkacomm/publisher/pdp-heartbeat_test.go b/pkg/kafkacomm/publisher/pdp-heartbeat_test.go
new file mode 100644 (file)
index 0000000..f03b0eb
--- /dev/null
@@ -0,0 +1,135 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package publisher
+
+import (
+       /*      "fmt"
+               "policy-opa-pdp/cfg"
+               "policy-opa-pdp/consts"
+               "policy-opa-pdp/pkg/log"
+               "policy-opa-pdp/pkg/model"
+               "policy-opa-pdp/pkg/pdpstate"*/
+       "errors"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+       "policy-opa-pdp/pkg/kafkacomm/publisher/mocks"
+       "testing"
+       //      "time"
+       /*      "github.com/google/uuid"*/)
+
+var (
+// ticker          *time.Ticker
+// stopChan        chan bool
+// currentInterval int64
+)
+
+/*
+Success Case 1
+TestStartHeartbeatIntervalTimer_ValidInterval
+Description: Test starting the heartbeat interval timer with a valid interval.
+Input: intervalMs = 1000
+Expected Output: The ticker starts with an interval of 1000 milliseconds, and heartbeat messages are sent at this interval.
+*/
+func TestStartHeartbeatIntervalTimer_ValidInterval(t *testing.T) {
+
+       intervalMs := int64(1000)
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(nil)
+
+       StartHeartbeatIntervalTimer(intervalMs, mockSender)
+       if ticker == nil {
+               t.Errorf("Expected ticker to be initialized")
+       }
+       if currentInterval != intervalMs {
+               t.Errorf("Expected currentInterval to be %d, got %d", intervalMs, currentInterval)
+       }
+}
+
+/*
+Failure Case 1
+TestStartHeartbeatIntervalTimer_InvalidInterval
+Description: Test starting the heartbeat interval timer with an invalid interval.
+Input: intervalMs = -1000
+Expected Output: The function should handle the invalid interval gracefully, possibly by logging an error message and not starting the ticker.
+*/
+func TestStartHeartbeatIntervalTimer_InvalidInterval(t *testing.T) {
+       intervalMs := int64(-1000)
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(nil)
+
+       StartHeartbeatIntervalTimer(intervalMs, mockSender)
+
+       if ticker != nil {
+               t.Log("Expected ticker to be nil for invalid interval")
+       }
+}
+
+/*
+TestSendPDPHeartBeat_Success 2
+Description: Test sending a heartbeat successfully.
+Input: Valid pdpStatus object
+Expected Output: Heartbeat message is sent successfully, and a debug log "Message sent successfully" is generated.
+*/
+func TestSendPDPHeartBeat_Success(t *testing.T) {
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(nil)
+       err := sendPDPHeartBeat(mockSender)
+       assert.NoError(t, err)
+}
+
+/*
+TestSendPDPHeartBeat_Failure 2
+Description: Test failing to send a heartbeat.
+Input: Invalid pdpStatus object or network failure
+Expected Output: An error occurs while sending the heartbeat, and a warning log "Error producing message: ..." is generated.
+*/
+func TestSendPDPHeartBeat_Failure(t *testing.T) {
+       // Mock SendPdpStatus to return an error
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("Error producing message"))
+       err := sendPDPHeartBeat(mockSender)
+       assert.Error(t, err)
+}
+
+/*
+TestStopTicker_Success 3
+Description: Test stopping the ticker.
+Input: Ticker is running
+Expected Output: The ticker stops, and the stop channel is closed.
+*/
+func TestStopTicker_Success(t *testing.T) {
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(nil)
+       StartHeartbeatIntervalTimer(1000, mockSender)
+       StopTicker()
+       if ticker != nil {
+               t.Errorf("Expected ticker to be nil")
+       }
+}
+
+/*
+TestStopTicker_NotRunning 3
+Description: Test stopping the ticker when it is not running.
+Input: Ticker is not running
+Expected Output: The function should handle this case gracefully, possibly by logging a debug message indicating that the ticker is not running.
+*/
+func TestStopTicker_NotRunning(t *testing.T) {
+       StopTicker()
+}
diff --git a/pkg/kafkacomm/publisher/pdp-pap-registration.go b/pkg/kafkacomm/publisher/pdp-pap-registration.go
new file mode 100644 (file)
index 0000000..75f22d6
--- /dev/null
@@ -0,0 +1,95 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// allows to send the pdp registartion message with unique transaction id and timestamp to topic
+package publisher
+
+import (
+       "encoding/json"
+       "fmt"
+       "github.com/google/uuid"
+       "policy-opa-pdp/cfg"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/kafkacomm"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/model"
+       "policy-opa-pdp/pkg/pdpattributes"
+       "time"
+)
+
+type PdpStatusSender interface {
+       SendPdpStatus(pdpStatus model.PdpStatus) error
+}
+
+type RealPdpStatusSender struct{}
+
+// Sends PdpSTatus Message type to KafkaTopic
+func (s *RealPdpStatusSender) SendPdpStatus(pdpStatus model.PdpStatus) error {
+
+       var topic string
+       bootstrapServers := cfg.BootstrapServer
+       topic = cfg.Topic
+       pdpStatus.RequestID = uuid.New().String()
+       pdpStatus.TimestampMs = fmt.Sprintf("%d", time.Now().UnixMilli())
+
+       jsonMessage, err := json.Marshal(pdpStatus)
+       if err != nil {
+               log.Warnf("failed to marshal PdpStatus to JSON: %v", err)
+               return err
+       }
+
+       producer, err := kafkacomm.GetKafkaProducer(bootstrapServers, topic)
+       if err != nil {
+               log.Warnf("Error creating Kafka producer: %v\n", err)
+               return err
+       }
+
+       err = producer.Produce(jsonMessage)
+       if err != nil {
+               log.Warnf("Error producing message: %v\n", err)
+       } else {
+               log.Debugf("[OUT|KAFKA|%s]\n%s", topic, string(jsonMessage))
+       }
+
+       return nil
+}
+
+// sends the registartion message to topic using SendPdpStatus(pdpStatus)
+func SendPdpPapRegistration(s PdpStatusSender) error {
+
+       var pdpStatus = model.PdpStatus{
+               MessageType: model.PDP_STATUS,
+               PdpType:     consts.PdpType,
+               State:       model.Passive,
+               Healthy:     model.Healthy,
+               Policies:    nil,
+               PdpResponse: nil,
+               Name:        pdpattributes.PdpName,
+               Description: "Pdp Status Registration Message",
+               PdpGroup:    consts.PdpGroup,
+       }
+
+       log.Debugf("Sending PDP PAP Registration Message")
+
+       err := s.SendPdpStatus(pdpStatus)
+       if err != nil {
+               log.Warnf("Error producing message: %v\n", err)
+               return err
+       }
+       return nil
+
+}
diff --git a/pkg/kafkacomm/publisher/pdp-pap-registration_test.go b/pkg/kafkacomm/publisher/pdp-pap-registration_test.go
new file mode 100644 (file)
index 0000000..03749de
--- /dev/null
@@ -0,0 +1,58 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package publisher
+
+import (
+       "errors"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+       "policy-opa-pdp/pkg/kafkacomm/publisher/mocks"
+       "policy-opa-pdp/pkg/model"
+       "testing"
+)
+
+type MockPdpStatusSender struct {
+       mock.Mock
+}
+
+func (m *MockPdpStatusSender) SendPdpStatus(pdpStatus model.PdpStatus) error {
+       return m.Called(pdpStatus).Error(0)
+
+}
+
+func TestSendPdpPapRegistration_Success(t *testing.T) {
+       mockSender := new(mocks.PdpStatusSender)
+
+       mockSender.On("SendPdpStatus", mock.AnythingOfType("model.PdpStatus")).Return(nil)
+
+       err := SendPdpPapRegistration(mockSender)
+       assert.NoError(t, err)
+       mockSender.AssertCalled(t, "SendPdpStatus", mock.AnythingOfType("model.PdpStatus"))
+}
+
+func TestSendPdpPapRegistration_Failure(t *testing.T) {
+       mockSender := new(mocks.PdpStatusSender)
+
+       mockSender.On("SendPdpStatus", mock.AnythingOfType("model.PdpStatus")).Return(errors.New("failed To Send"))
+
+       err := SendPdpPapRegistration(mockSender)
+       assert.Error(t, err, "Expected an error for failure")
+       assert.EqualError(t, err, "failed To Send", "Error messages should match")
+       mockSender.AssertCalled(t, "SendPdpStatus", mock.AnythingOfType("model.PdpStatus"))
+}
diff --git a/pkg/kafkacomm/publisher/pdp-status-publisher.go b/pkg/kafkacomm/publisher/pdp-status-publisher.go
new file mode 100644 (file)
index 0000000..756d0f2
--- /dev/null
@@ -0,0 +1,109 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+// responsible for sending PDP_STATUS messages in response to specific events
+// such as updates (PDP_UPDATE) or state changes (PDP_STATE_CHANGE). These responses provide details
+// about the current state, health, and attributes of the Policy Decision Point (PDP).
+package publisher
+
+import (
+       "fmt"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/model"
+       "policy-opa-pdp/pkg/pdpattributes"
+       "policy-opa-pdp/pkg/pdpstate"
+       "time"
+
+       "github.com/google/uuid"
+)
+
+// Sends a PDP_STATUS message to indicate the successful processing of a PDP_UPDATE request
+// received from the Policy Administration Point (PAP).
+func SendPdpUpdateResponse(s PdpStatusSender, pdpUpdate *model.PdpUpdate) error {
+
+       responseStatus := model.Success
+       responseMessage := "PDP Update was Successful"
+
+       pdpStatus := model.PdpStatus{
+               MessageType: model.PDP_STATUS,
+               PdpType:     consts.PdpType,
+               State:       pdpstate.State,
+               Healthy:     model.Healthy,
+               Name:        pdpattributes.PdpName,
+               Description: "Pdp Status Response Message For Pdp Update",
+               PdpGroup:    consts.PdpGroup,
+               PdpSubgroup: &pdpattributes.PdpSubgroup,
+               // Policies: [],
+               PdpResponse: &model.PdpResponseDetails{
+                       ResponseTo:      &pdpUpdate.RequestId,
+                       ResponseStatus:  &responseStatus,
+                       ResponseMessage: &responseMessage,
+               },
+       }
+
+       pdpStatus.RequestID = uuid.New().String()
+       pdpStatus.TimestampMs = fmt.Sprintf("%d", time.Now().UnixMilli())
+
+       log.Infof("Sending PDP Status With Update Response")
+
+       err := s.SendPdpStatus(pdpStatus)
+       if err != nil {
+               log.Warnf("Failed to send PDP Update Message : %v", err)
+               return err
+       }
+
+       return nil
+
+}
+
+// Sends a PDP_STATUS message to indicate a state change in the PDP (e.g., from PASSIVE to ACTIVE).
+func SendStateChangeResponse(s PdpStatusSender, pdpStateChange *model.PdpStateChange) error {
+
+       responseStatus := model.Success
+       responseMessage := "PDP State Changed From PASSIVE TO Active"
+       pdpStatus := model.PdpStatus{
+               MessageType: model.PDP_STATUS,
+               PdpType:     consts.PdpType,
+               State:       pdpstate.GetState(),
+               Healthy:     model.Healthy,
+               Name:        pdpattributes.PdpName,
+               Description: "Pdp Status Response Message to Pdp State Change",
+               PdpGroup:    consts.PdpGroup,
+               PdpSubgroup: &pdpattributes.PdpSubgroup,
+               // Policies: [],
+               PdpResponse: &model.PdpResponseDetails{
+                       ResponseTo:      &pdpStateChange.RequestId,
+                       ResponseStatus:  &responseStatus,
+                       ResponseMessage: &responseMessage,
+               },
+       }
+
+       pdpStatus.RequestID = uuid.New().String()
+       pdpStatus.TimestampMs = fmt.Sprintf("%d", time.Now().UnixMilli())
+
+       log.Infof("Sending PDP Status With State Change response")
+
+       err := s.SendPdpStatus(pdpStatus)
+       if err != nil {
+               log.Warnf("Failed to send PDP Update Message : %v", err)
+               return err
+       }
+
+       return nil
+}
diff --git a/pkg/kafkacomm/publisher/pdp-status-publisher_test.go b/pkg/kafkacomm/publisher/pdp-status-publisher_test.go
new file mode 100644 (file)
index 0000000..5e02704
--- /dev/null
@@ -0,0 +1,83 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package publisher
+
+import (
+       "errors"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+       "policy-opa-pdp/pkg/kafkacomm/publisher/mocks"
+       "policy-opa-pdp/pkg/model"
+       "testing"
+)
+
+// TestSendPdpUpdateResponse_Success tests SendPdpUpdateResponse for a successful response
+func TestSendPdpUpdateResponse_Success(t *testing.T) {
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(nil)
+       pdpUpdate := &model.PdpUpdate{RequestId: "test-request-id"}
+
+       err := SendPdpUpdateResponse(mockSender, pdpUpdate)
+       assert.NoError(t, err)
+       mockSender.AssertCalled(t, "SendPdpStatus", mock.Anything)
+}
+
+// TestSendPdpUpdateResponse_Failure tests SendPdpUpdateResponse when SendPdpStatus fails
+func TestSendPdpUpdateResponse_Failure(t *testing.T) {
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("mock send error"))
+
+       pdpUpdate := &model.PdpUpdate{RequestId: "test-request-id"}
+
+       err := SendPdpUpdateResponse(mockSender, pdpUpdate)
+
+       assert.Error(t, err)
+
+       mockSender.AssertCalled(t, "SendPdpStatus", mock.Anything)
+}
+
+// TestSendStateChangeResponse_Success tests SendStateChangeResponse for a successful state change response
+func TestSendStateChangeResponse_Success(t *testing.T) {
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(nil)
+
+       pdpStateChange := &model.PdpStateChange{RequestId: "test-state-change-id"}
+
+       err := SendStateChangeResponse(mockSender, pdpStateChange)
+
+       assert.NoError(t, err)
+       mockSender.AssertCalled(t, "SendPdpStatus", mock.Anything)
+}
+
+// TestSendStateChangeResponse_Failure tests SendStateChangeResponse when SendPdpStatus fails
+func TestSendStateChangeResponse_Failure(t *testing.T) {
+
+       mockSender := new(mocks.PdpStatusSender)
+       mockSender.On("SendPdpStatus", mock.Anything).Return(errors.New("mock send error"))
+
+       pdpStateChange := &model.PdpStateChange{RequestId: "test-state-change-id"}
+
+       err := SendStateChangeResponse(mockSender, pdpStateChange)
+       assert.Error(t, err)
+       mockSender.AssertCalled(t, "SendPdpStatus", mock.Anything)
+
+}
diff --git a/pkg/log/log.go b/pkg/log/log.go
new file mode 100644 (file)
index 0000000..2a8b997
--- /dev/null
@@ -0,0 +1,131 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package log
+
+import (
+       "github.com/sirupsen/logrus"
+       "gopkg.in/natefinch/lumberjack.v2"
+       "io"
+       "os"
+       "policy-opa-pdp/cfg"
+       "policy-opa-pdp/consts"
+)
+
+type Logger struct {
+       *logrus.Logger
+}
+
+var (
+       Log *Logger
+)
+
+func SetOutput(w io.Writer) {
+       Log.SetOutput(w)
+}
+
+func init() {
+       Log = InitLogger(consts.LogFilePath, consts.LogMaxSize, consts.LogMaxBackups, cfg.LogLevel)
+}
+
+func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logLevel string) *Logger {
+       log := logrus.New()
+
+       log.SetLevel(logrus.DebugLevel)
+       log.SetOutput(os.Stdout)
+
+       logLevelParsed, err := logrus.ParseLevel(logLevel)
+       if err != nil {
+               log.Warn(err)
+       }
+       log.SetLevel(logLevelParsed)
+
+       logRotation := &lumberjack.Logger{
+               Filename:   consts.LogFilePath,
+               MaxSize:    consts.LogMaxSize,
+               MaxBackups: consts.LogMaxBackups,
+       }
+       multiWriter := io.MultiWriter(os.Stdout, logRotation)
+       log.SetOutput(multiWriter)
+
+       log.SetFormatter(&logrus.TextFormatter{
+               ForceColors:     true,
+               DisableColors:   false,
+               FullTimestamp:   true,
+               TimestampFormat: "2006-01-02T15:04:05.0000-07:00",
+       })
+
+       log.Debugf("logger initialised Filepath = %s, Logsize(MB) = %d, Backups = %d, Loglevel = %s", logFilePath, logMaxSize, logMaxBackups, logLevel)
+       return &Logger{log}
+}
+
+func ParseLevel(level string) (logrus.Level, error) {
+       return logrus.ParseLevel(level)
+}
+
+func SetLevel(level logrus.Level) {
+       Log.SetLevel(level)
+}
+
+func Error(args ...interface{}) {
+       Log.Error(args...)
+}
+
+func Info(args ...interface{}) {
+       Log.Info(args...)
+}
+
+func Debug(args ...interface{}) {
+       Log.Debug(args...)
+}
+
+func Warn(args ...interface{}) {
+       Log.Warn(args...)
+}
+
+func Panic(args ...interface{}) {
+       Log.Panic(args...)
+}
+
+func Trace(args ...interface{}) {
+       Log.Trace(args...)
+}
+
+func Errorf(msg string, args ...interface{}) {
+       Log.Errorf(msg, args...)
+}
+
+func Infof(msg string, args ...interface{}) {
+       Log.Infof(msg, args...)
+}
+
+func Debugf(msg string, args ...interface{}) {
+       Log.Debugf(msg, args...)
+}
+
+func Warnf(msg string, args ...interface{}) {
+       Log.Warnf(msg, args...)
+}
+
+func Panicf(msg string, args ...interface{}) {
+       Log.Panicf(msg, args...)
+}
+
+func Tracef(msg string, args ...interface{}) {
+       Log.Tracef(msg, args...)
+}
diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go
new file mode 100644 (file)
index 0000000..d24274c
--- /dev/null
@@ -0,0 +1,354 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package log_test
+
+import (
+       "testing"
+
+       "bytes"
+       "github.com/sirupsen/logrus"
+       "policy-opa-pdp/pkg/log"
+)
+
+func TestSetOutput_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.InfoLevel)
+
+       log.Info("Testing SetOutput")
+       if !bytes.Contains(buf.Bytes(), []byte("Testing SetOutput")) {
+               t.Errorf("Expected message to be logged")
+       }
+}
+
+func TestInit_Success(t *testing.T) {
+       var buf bytes.Buffer
+
+       log.SetOutput(&buf)
+       log.InitLogger("/tmp/logfile.log", 10, 5, "debug")
+       log.Info("Logger initialized")
+
+       if !bytes.Contains(buf.Bytes(), []byte("Logger initialized")) {
+               t.Errorf("Expected message to be logged after initialization")
+       }
+}
+
+func TestInitLogger_Success(t *testing.T) {
+       var buf bytes.Buffer
+
+       log.SetOutput(&buf)
+
+       log.InitLogger("/tmp/logfile.log", 10, 5, "info")
+
+       log.Info("Logger Initialized Test")
+       if !bytes.Contains(buf.Bytes(), []byte("Logger Initialized Test")) {
+               t.Errorf("Expected message to be logged")
+       }
+}
+
+func TestParseLevel_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+
+       level, err := logrus.ParseLevel("info")
+       if err != nil {
+               t.Fatalf("Failed to parse log level: %v", err)
+       }
+       log.SetLevel(level)
+
+       log.Info("Info level set")
+
+       if !bytes.Contains(buf.Bytes(), []byte("Info level set")) {
+               t.Errorf("Expected info level to be set")
+       }
+}
+
+func TestSetLevel_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.DebugLevel)
+
+       log.Debug("This is a debug message")
+       if !bytes.Contains(buf.Bytes(), []byte("This is a debug message")) {
+               t.Errorf("Expected debug message to be logged")
+       }
+}
+
+func TestError_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.ErrorLevel)
+
+       log.Error("This is an error message")
+       if !bytes.Contains(buf.Bytes(), []byte("This is an error message")) {
+               t.Errorf("Expected error message to be logged")
+       }
+}
+
+func TestInfo_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.InfoLevel)
+
+       log.Info("This is an info message")
+       if !bytes.Contains(buf.Bytes(), []byte("This is an info message")) {
+               t.Errorf("Expected info message to be logged")
+       }
+}
+
+func TestDebug_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.DebugLevel)
+
+       log.Debug("This is a debug message")
+       if !bytes.Contains(buf.Bytes(), []byte("This is a debug message")) {
+               t.Errorf("Expected debug message to be logged")
+       }
+}
+
+func TestWarn_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.WarnLevel)
+
+       log.Warn("This is a warning message")
+       if !bytes.Contains(buf.Bytes(), []byte("This is a warning message")) {
+               t.Errorf("Expected warning message to be logged")
+       }
+}
+
+func TestPanic_Success(t *testing.T) {
+       defer func() {
+               if r := recover(); r == nil {
+                       t.Errorf("Expected panic, but did not get one")
+               }
+       }()
+
+       log.SetLevel(logrus.PanicLevel)
+       log.Panic("This is a panic message")
+}
+
+func TestTrace_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.TraceLevel)
+
+       log.Trace("This is a trace message")
+       if !bytes.Contains(buf.Bytes(), []byte("This is a trace message")) {
+               t.Errorf("Expected trace message to be logged")
+       }
+}
+
+func TestErrorf_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.ErrorLevel)
+
+       log.Errorf("Error occurred: %s", "test error")
+       if !bytes.Contains(buf.Bytes(), []byte("Error occurred: test error")) {
+               t.Errorf("Expected error message to be logged")
+       }
+}
+
+func TestInfof_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.InfoLevel)
+
+       log.Infof("Info log: %s", "test info")
+       if !bytes.Contains(buf.Bytes(), []byte("Info log: test info")) {
+               t.Errorf("Expected info message to be logged")
+       }
+}
+
+func TestDebugf_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.DebugLevel)
+
+       log.Debugf("Debug message: %s", "should log")
+       if !bytes.Contains(buf.Bytes(), []byte("Debug message: should log")) {
+               t.Errorf("Expected debug message to be logged")
+       }
+}
+
+func TestWarnf_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.WarnLevel)
+
+       log.Warnf("Warning message: %s", "should log")
+       if !bytes.Contains(buf.Bytes(), []byte("Warning message: should log")) {
+               t.Errorf("Expected warning message to be logged")
+       }
+}
+
+func TestPanicf_Success(t *testing.T) {
+       defer func() {
+               if r := recover(); r == nil {
+                       t.Errorf("Expected panic, but did not get one")
+               }
+       }()
+
+       log.SetLevel(logrus.PanicLevel)
+       log.Panicf("Panic message: %s", "should panic")
+}
+
+func TestTracef_Success(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.TraceLevel)
+
+       log.Tracef("Trace message: %s", "should log")
+       if !bytes.Contains(buf.Bytes(), []byte("Trace message: should log")) {
+               t.Errorf("Expected trace message to be logged")
+       }
+}
+
+func TestError_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.FatalLevel) // Set level higher than Error
+
+       log.Error("This is an error message")
+       if bytes.Contains(buf.Bytes(), []byte("This is an error message")) {
+               t.Errorf("Expected error message not to be logged")
+       }
+}
+
+func TestInfo_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.WarnLevel) // Set level higher than Info
+
+       log.Info("This is an info message")
+       if bytes.Contains(buf.Bytes(), []byte("This is an info message")) {
+               t.Errorf("Expected info message not to be logged")
+       }
+}
+
+func TestDebug_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.InfoLevel) // Set level higher than Debug
+
+       log.Debug("This is a debug message")
+       if bytes.Contains(buf.Bytes(), []byte("This is a debug message")) {
+               t.Errorf("Expected debug message not to be logged")
+       }
+}
+
+func TestWarn_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.ErrorLevel) // Set level higher than Warn
+
+       log.Warn("This is a warning message")
+       if bytes.Contains(buf.Bytes(), []byte("This is a warning message")) {
+               t.Errorf("Expected warning message not to be logged")
+       }
+}
+
+func TestPanic_Failure(t *testing.T) {
+       defer func() {
+               if r := recover(); r == nil {
+                       t.Errorf("Expected a panic at PanicLevel, but did not get one")
+               }
+       }()
+       log.SetLevel(logrus.PanicLevel) // Set to PanicLevel so a panic should occur
+       log.Panic("This should cause a panic at PanicLevel")
+}
+
+func TestTrace_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.DebugLevel) // Set level higher than Trace
+
+       log.Trace("This is a trace message")
+       if bytes.Contains(buf.Bytes(), []byte("This is a trace message")) {
+               t.Errorf("Expected trace message not to be logged")
+       }
+}
+
+func TestErrorf_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.FatalLevel) // Set level higher than Error
+
+       log.Errorf("Error occurred: %s", "test error")
+       if bytes.Contains(buf.Bytes(), []byte("Error occurred: test error")) {
+               t.Errorf("Expected error message not to be logged")
+       }
+}
+
+func TestInfof_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.WarnLevel) // Set level higher than Info
+
+       log.Infof("Info log: %s", "test info")
+       if bytes.Contains(buf.Bytes(), []byte("Info log: test info")) {
+               t.Errorf("Expected info message not to be logged")
+       }
+}
+
+func TestDebugf_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.InfoLevel) // Set level higher than Debug
+
+       log.Debugf("Debug message: %s", "should not log")
+       if bytes.Contains(buf.Bytes(), []byte("Debug message: should not log")) {
+               t.Errorf("Expected debug message not to be logged")
+       }
+}
+
+func TestWarnf_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.ErrorLevel) // Set level higher than Warn
+
+       log.Warnf("Warning message: %s", "should not log")
+       if bytes.Contains(buf.Bytes(), []byte("Warning message: should not log")) {
+               t.Errorf("Expected warning message not to be logged")
+       }
+}
+
+func TestPanicf_Failure(t *testing.T) {
+       defer func() {
+               if r := recover(); r == nil {
+                       t.Errorf("Expected a panic at PanicLevel, but did not get one")
+               }
+       }()
+
+       log.SetLevel(logrus.PanicLevel) // Set to PanicLevel so a panic should occur
+       log.Panicf("Panicf message: %s", "should panic at PanicLevel")
+}
+
+func TestTracef_Failure(t *testing.T) {
+       var buf bytes.Buffer
+       log.SetOutput(&buf)
+       log.SetLevel(logrus.DebugLevel) // Set level higher than Trace
+
+       log.Tracef("Trace message: %s", "should not log")
+       if bytes.Contains(buf.Bytes(), []byte("Trace message: should not log")) {
+               t.Errorf("Expected trace message not to be logged")
+       }
+}
diff --git a/pkg/metrics/counters.go b/pkg/metrics/counters.go
new file mode 100644 (file)
index 0000000..2fc9539
--- /dev/null
@@ -0,0 +1,66 @@
+package metrics
+
+import "sync"
+
+//global counter variables
+var IndeterminantDecisionsCount int64
+var PermitDecisionsCount int64
+var DenyDecisionsCount int64
+var TotalErrorCount int64
+var mu sync.Mutex
+
+// Increment counter
+func IncrementIndeterminantDecisionsCount() {
+       mu.Lock()
+       IndeterminantDecisionsCount++
+       mu.Unlock()
+}
+
+// returns pointer to the counter
+func IndeterminantDecisionsCountRef() *int64 {
+       mu.Lock()
+       defer mu.Unlock()
+       return &IndeterminantDecisionsCount
+}
+
+// Increment counter
+func IncrementPermitDecisionsCount() {
+       mu.Lock()
+       PermitDecisionsCount++
+       mu.Unlock()
+}
+
+// returns pointer to the counter
+func PermitDecisionsCountRef() *int64 {
+       mu.Lock()
+       defer mu.Unlock()
+       return &PermitDecisionsCount
+}
+
+// Increment counter
+func IncrementDenyDecisionsCount() {
+       mu.Lock()
+       DenyDecisionsCount++
+       mu.Unlock()
+}
+
+// returns pointer to the counter
+func DenyDecisionsCountRef() *int64 {
+       mu.Lock()
+       defer mu.Unlock()
+       return &DenyDecisionsCount
+}
+
+// Increment counter
+func IncrementTotalErrorCount() {
+       mu.Lock()
+       TotalErrorCount++
+       mu.Unlock()
+}
+
+// returns pointer to the counter
+func TotalErrorCountRef() *int64 {
+       mu.Lock()
+       defer mu.Unlock()
+       return &TotalErrorCount
+}
diff --git a/pkg/metrics/counters_test.go b/pkg/metrics/counters_test.go
new file mode 100644 (file)
index 0000000..ef4c2b0
--- /dev/null
@@ -0,0 +1,60 @@
+package metrics
+
+import (
+       "sync"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestCounters(t *testing.T) {
+       var wg sync.WaitGroup
+
+       // Test IncrementIndeterminantDecisionsCount and IndeterminantDecisionsCountRef
+       IndeterminantDecisionsCount = 0
+       wg.Add(10)
+       for i := 0; i < 10; i++ {
+               go func() {
+                       defer wg.Done()
+                       IncrementIndeterminantDecisionsCount()
+               }()
+       }
+       wg.Wait()
+       assert.Equal(t, int64(10), *IndeterminantDecisionsCountRef())
+
+       // Test IncrementPermitDecisionsCount and PermitDecisionsCountRef
+       PermitDecisionsCount = 0
+       wg.Add(15)
+       for i := 0; i < 15; i++ {
+               go func() {
+                       defer wg.Done()
+                       IncrementPermitDecisionsCount()
+               }()
+       }
+       wg.Wait()
+       assert.Equal(t, int64(15), *PermitDecisionsCountRef())
+
+       // Test IncrementDenyDecisionsCount and DenyDecisionsCountRef
+       DenyDecisionsCount = 0
+       wg.Add(20)
+       for i := 0; i < 20; i++ {
+               go func() {
+                       defer wg.Done()
+                       IncrementDenyDecisionsCount()
+               }()
+       }
+       wg.Wait()
+       assert.Equal(t, int64(20), *DenyDecisionsCountRef())
+
+       // Test IncrementTotalErrorCount and TotalErrorCountRef
+       TotalErrorCount = 0
+       wg.Add(5)
+       for i := 0; i < 5; i++ {
+               go func() {
+                       defer wg.Done()
+                       IncrementTotalErrorCount()
+               }()
+       }
+       wg.Wait()
+       assert.Equal(t, int64(5), *TotalErrorCountRef())
+}
diff --git a/pkg/metrics/statistics-provider.go b/pkg/metrics/statistics-provider.go
new file mode 100644 (file)
index 0000000..67cee79
--- /dev/null
@@ -0,0 +1,65 @@
+// Handles an HTTP request to fetch the current system statistics.
+// It aggregates various decision counts (e.g., indeterminate, permit, deny)
+// and error counts into a structured response and sends it back to the client in JSON format.
+package metrics
+
+import (
+       "encoding/json"
+       "net/http"
+       "policy-opa-pdp/pkg/log"
+       "policy-opa-pdp/pkg/model/oapicodegen"
+       "policy-opa-pdp/pkg/utils"
+
+       "github.com/google/uuid"
+       openapi_types "github.com/oapi-codegen/runtime/types"
+)
+
+func FetchCurrentStatistics(res http.ResponseWriter, req *http.Request) {
+
+       requestId := req.Header.Get("X-ONAP-RequestID")
+       var parsedUUID *uuid.UUID
+       var statisticsParams *oapicodegen.StatisticsParams
+
+       if requestId != "" && utils.IsValidUUID(requestId) {
+               tempUUID, err := uuid.Parse(requestId)
+               if err != nil {
+                       log.Warnf("Error Parsing the requestID: %v", err)
+               } else {
+                       parsedUUID = &tempUUID
+                       statisticsParams = &oapicodegen.StatisticsParams{
+                               XONAPRequestID: (*openapi_types.UUID)(parsedUUID),
+                       }
+                       res.Header().Set("X-ONAP-RequestID", statisticsParams.XONAPRequestID.String())
+               }
+       } else {
+               log.Warnf("Invalid or Missing  Request ID")
+               requestId = "000000000000"
+               res.Header().Set("X-ONAP-RequestID", requestId)
+       }
+
+       var statReport oapicodegen.StatisticsReport
+
+       statReport.IndeterminantDecisionsCount = IndeterminantDecisionsCountRef()
+       statReport.PermitDecisionsCount = PermitDecisionsCountRef()
+       statReport.DenyDecisionsCount = DenyDecisionsCountRef()
+       statReport.TotalErrorCount = TotalErrorCountRef()
+
+       // not implemented hardcoding the values to zero
+       // will be implemeneted in phase-2
+       zerovalue := int64(0)
+       onevalue := int64(1)
+       statReport.TotalPoliciesCount = &zerovalue
+       statReport.TotalPolicyTypesCount = &onevalue
+       statReport.DeployFailureCount = &zerovalue
+       statReport.DeploySuccessCount = &zerovalue
+       statReport.UndeployFailureCount = &zerovalue
+       statReport.UndeploySuccessCount = &zerovalue
+
+       value := int32(200)
+       statReport.Code = &value
+
+       res.Header().Set("Content-Type", "application/json")
+       res.WriteHeader(http.StatusOK)
+       json.NewEncoder(res).Encode(statReport)
+
+}
diff --git a/pkg/metrics/statistics-provider_test.go b/pkg/metrics/statistics-provider_test.go
new file mode 100644 (file)
index 0000000..a5e57b6
--- /dev/null
@@ -0,0 +1,51 @@
+package metrics
+
+import (
+       "encoding/json"
+       "net/http"
+       "net/http/httptest"
+       "policy-opa-pdp/pkg/model/oapicodegen"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestFetchCurrentStatistics(t *testing.T) {
+
+       IndeterminantDecisionsCount = 10
+       PermitDecisionsCount = 15
+       DenyDecisionsCount = 20
+       TotalErrorCount = 5
+
+       // Create a new HTTP request
+       req := httptest.NewRequest(http.MethodGet, "/statistics", nil)
+       // Create a response recorder to capture the response
+       res := httptest.NewRecorder()
+
+       // Call the function under test
+       FetchCurrentStatistics(res, req)
+
+       // Verify the status code
+       assert.Equal(t, http.StatusOK, res.Code)
+
+       // Verify the response headers
+       assert.Equal(t, "application/json", res.Header().Get("Content-Type"))
+
+       var statReport oapicodegen.StatisticsReport
+       err := json.Unmarshal(res.Body.Bytes(), &statReport)
+       assert.NoError(t, err)
+
+       // Verify the response body
+       assert.Equal(t, int64(10), *statReport.IndeterminantDecisionsCount)
+       assert.Equal(t, int64(15), *statReport.PermitDecisionsCount)
+       assert.Equal(t, int64(20), *statReport.DenyDecisionsCount)
+       assert.Equal(t, int64(5), *statReport.TotalErrorCount)
+       assert.Equal(t, int64(0), *statReport.TotalPoliciesCount)
+       assert.Equal(t, int64(1), *statReport.TotalPolicyTypesCount)
+       assert.Equal(t, int64(0), *statReport.DeployFailureCount)
+       assert.Equal(t, int64(0), *statReport.DeploySuccessCount)
+       assert.Equal(t, int64(0), *statReport.UndeployFailureCount)
+       assert.Equal(t, int64(0), *statReport.UndeploySuccessCount)
+
+       assert.Equal(t, int32(200), *statReport.Code)
+}
diff --git a/pkg/model/healthcheckmessage.go b/pkg/model/healthcheckmessage.go
new file mode 100644 (file)
index 0000000..8b0d9db
--- /dev/null
@@ -0,0 +1,27 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+package model
+type HealthCheckResponse struct {
+        Name    string `json:"name"`
+        Url     string `json:"url"`
+        Healthy bool   `json:"healthy"`
+        Code    int    `json:"code"`
+        Message string `json:"message"`
+}
diff --git a/pkg/model/mesages.go b/pkg/model/mesages.go
new file mode 100644 (file)
index 0000000..a4451d7
--- /dev/null
@@ -0,0 +1,111 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Defines structure for messages exchanged between PDP and PAP
+// Refer: https://docs.onap.org/projects/onap-policy-parent/en/latest/pap/InternalPapPdp.html
+// for attribute level details of each message.
+package model
+
+import (
+       "encoding/json"
+       "fmt"
+)
+
+// PdpMessageType represents the type of PDP message.
+type PdpMessageType int
+
+// Enumerate the possible PDP message types
+// https://github.com/onap/policy-models
+// models-pdp/src/main/java/org/onap/policy/models/pdp/enums/PdpMessageType.java
+const (
+       PDP_STATUS PdpMessageType = iota
+       PDP_UPDATE
+       PDP_STATE_CHANGE
+       PDP_HEALTH_CHECK
+       PDP_TOPIC_CHECK
+)
+
+// String representation of PdpMessageType
+func (msgType PdpMessageType) String() string {
+       switch msgType {
+       case PDP_STATUS:
+               return "PDP_STATUS"
+       case PDP_UPDATE:
+               return "PDP_UPDATE"
+       case PDP_STATE_CHANGE:
+               return "PDP_STATE_CHANGE"
+       case PDP_HEALTH_CHECK:
+               return "PDP_HEALTH_CHECK"
+       case PDP_TOPIC_CHECK:
+               return "PDP_TOPIC_CHECK"
+       default:
+               return fmt.Sprintf("Unknown PdpMessageType: %d", msgType)
+       }
+}
+
+func (p PdpMessageType) MarshalJSON() ([]byte, error) {
+       return json.Marshal(p.String())
+}
+
+// PdpStatus represents the PDP_STATUS message sent from PDP to PAP.
+// https://github.com/onap/policy-models
+// models-pdp/src/main/java/org/onap/policy/models/pdp/concepts/PdpStatus.java
+type PdpStatus struct {
+       MessageType PdpMessageType           `json:"messageName"`
+       PdpType     string                   `json:"pdpType"`
+       State       PdpState                 `json:"state"`
+       Healthy     PdpHealthStatus          `json:"healthy"`
+       Description string                   `json:"description"`
+       PdpResponse *PdpResponseDetails      `json:"response"`
+       Policies    []ToscaConceptIdentifier `json:"policies"`
+       Name        string                   `json:"name"`
+       RequestID   string                   `json:"requestId"`
+       PdpGroup    string                   `json:"pdpGroup"`
+       PdpSubgroup *string                  `json:"pdpSubgroup"`
+       TimestampMs string                   `json:"timestampMs"`
+       DeploymentInstanceInfo string        `json:"deploymentInstanceInfo"`
+}
+
+// PDP_UPDATE sent by PAP to PDP.
+// https://github.com/onap/policy-models
+// models-pdp/src/main/java/org/onap/policy/models/pdp/concepts/PdpUpdate.java
+type PdpUpdate struct {
+       Source                 string                    `json:"source" validate:"required"`
+       PdpHeartbeatIntervalMs int64                     `json:"pdpHeartbeatIntervalMs" validate:"required"`
+       MessageType            string                    `json:"messageName" validate:"required"`
+       PoliciesToBeDeloyed    []string                  `json:"policiesToBeDeployed" validate:"required"`
+       policiesToBeUndeployed []ToscaConceptIdentifier  `json:"policiesToBeUndeployed"`
+       Name                   string                    `json:"name" validate:"required"`
+       TimestampMs            int64                     `json:"timestampMs" validate:"required"`
+       PdpGroup               string                    `json:"pdpGroup" validate:"required"`
+       PdpSubgroup            string                    `json:"pdpSubgroup" validate:"required"`
+       RequestId              string                    `json:"requestId" validate:"required"`
+}
+
+// PDP_STATE_CHANGE sent by PAP to PDP.
+// https://github.com/onap/policy-models
+// models-pdp/src/main/java/org/onap/policy/models/pdp/concepts/PdpStateChange.java
+type PdpStateChange struct {
+       Source      string `json:"source"`
+       State       string `json:"state"`
+       MessageType string `json:"messageName"`
+       Name        string `json:"name"`
+       TimestampMs int64  `json:"timestampMs"`
+       PdpGroup    string `json:"pdpGroup"`
+       PdpSubgroup string `json:"pdpSubgroup"`
+       RequestId   string `json:"requestId"`
+}
diff --git a/pkg/model/messages_test.go b/pkg/model/messages_test.go
new file mode 100644 (file)
index 0000000..f6bb5ca
--- /dev/null
@@ -0,0 +1,241 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package model
+
+import (
+       "encoding/json"
+       "errors"
+       "testing"
+)
+
+func (p *PdpStatus) Validate() error {
+       if p.PdpType == "" {
+               return errors.New("PdpType is required")
+       }
+
+       // Check if State is set to a valid non-zero value
+       if p.State != Passive && p.State != Safe && p.State != Test && p.State != Active && p.State != Terminated {
+               return errors.New("State is required and must be a valid PdpState")
+       }
+
+       // Check if Healthy is set to a valid non-zero value
+       if p.Healthy != Healthy && p.Healthy != NotHealthy && p.Healthy != TestInProgress && p.Healthy != Unknown {
+               return errors.New("Healthy status is required and must be a valid PdpHealthStatus")
+       }
+
+       if p.Name == "" {
+               return errors.New("Name is required")
+       }
+       if p.RequestID == "" {
+               return errors.New("RequestID is required")
+       }
+       if p.PdpGroup == "" {
+               return errors.New("PdpGroup is required")
+       }
+       if p.TimestampMs == "" {
+               return errors.New("TimestampMs is required")
+       }
+
+       return nil
+}
+
+// TestPdpStatusSerialization_Positive tests the successful serialization of PdpStatus.
+func TestPdpStatusSerialization_Success(t *testing.T) {
+       pdpStatus := PdpStatus{
+               MessageType: PDP_STATUS,
+               PdpType:     "examplePdpType",
+               State:       Active,
+               Healthy:     Healthy,
+               Description: "PDP is healthy",
+               PdpResponse: nil, // Set to nil for simplicity
+               Policies:    []ToscaConceptIdentifier{},
+               Name:        "ExamplePDP",
+               RequestID:   "12345",
+               PdpGroup:    "Group1",
+               PdpSubgroup: nil,
+               TimestampMs: "1633017600000",
+       }
+
+       _, err := json.Marshal(pdpStatus)
+       if err != nil {
+               t.Errorf("Expected no error while marshaling valid PdpStatus, got: %v", err)
+       }
+}
+
+// TestPdpStatusSerialization_Negative tests the serialization of PdpStatus with invalid fields.
+func TestPdpStatusValidation_Failure(t *testing.T) {
+       // Example of invalid state and health strings that will fail conversion
+       state, err := ConvertStringToEnumState("INVALID_STATE")
+       if err == nil {
+               t.Fatal("Expected error for invalid state")
+       }
+
+       // Example with missing fields or invalid enums
+       pdpStatus := PdpStatus{
+               PdpType:     "",
+               State:       state,
+               Name:        "",
+               RequestID:   "",
+               PdpGroup:    "",
+               TimestampMs: "",
+       }
+
+       err = pdpStatus.Validate()
+       if err == nil {
+               t.Error("Expected an error while validating invalid PdpStatus, but got none")
+       }
+}
+
+func (p *PdpUpdate) Validate() error {
+       if p.Source == "" {
+               return errors.New("Source is required")
+       }
+       if p.PdpHeartbeatIntervalMs <= 0 {
+               return errors.New("PdpHeartbeatIntervalMs must be a positive integer")
+       }
+       if p.MessageType == "" {
+               return errors.New("MessageType is required")
+       }
+       if len(p.PoliciesToBeDeloyed) == 0 {
+               return errors.New("PoliciesToBeDeloyed is required and must contain at least one policy")
+       }
+       if p.Name == "" {
+               return errors.New("Name is required")
+       }
+       if p.TimestampMs <= 0 {
+               return errors.New("TimestampMs is required and must be a positive integer")
+       }
+       if p.PdpGroup == "" {
+               return errors.New("PdpGroup is required")
+       }
+       if p.PdpSubgroup == "" {
+               return errors.New("PdpSubgroup is required")
+       }
+       if p.RequestId == "" {
+               return errors.New("RequestId is required")
+       }
+
+       return nil
+}
+
+// TestPdpUpdateSerialization_Positive tests the successful serialization of PdpUpdate.
+func TestPdpUpdateSerialization_Success(t *testing.T) {
+       pdpUpdate := PdpUpdate{
+               Source:                 "source1",
+               PdpHeartbeatIntervalMs: 5000,
+               MessageType:            "PDP_UPDATE",
+               PoliciesToBeDeloyed:    []string{"policy1", "policy2"},
+               Name:                   "ExamplePDP",
+               TimestampMs:            1633017600000,
+               PdpGroup:               "Group1",
+               PdpSubgroup:            "SubGroup1",
+               RequestId:              "54321",
+       }
+
+       _, err := json.Marshal(pdpUpdate)
+       if err != nil {
+               t.Errorf("Expected no error while marshaling valid PdpUpdate, got: %v", err)
+       }
+}
+
+// TestPdpUpdateSerialization_Negative tests the serialization of PdpUpdate with invalid fields.
+func TestPdpUpdateSerialization_Failure(t *testing.T) {
+       pdpUpdate := PdpUpdate{
+               Source:                 "",
+               PdpHeartbeatIntervalMs: 5000,
+               MessageType:            "",
+               PoliciesToBeDeloyed:    nil,
+               Name:                   "",
+               TimestampMs:            0,
+               PdpGroup:               "",
+               PdpSubgroup:            "",
+               RequestId:              "",
+       }
+       err := pdpUpdate.Validate()
+       if err == nil {
+               t.Error("Expected an error while validating invalid PdpStatus, but got none")
+       }
+
+}
+
+func (p *PdpStateChange) Validate() error {
+       if p.Source == "" {
+               return errors.New("Source is required")
+       }
+       if p.State == "" {
+               return errors.New("State is required")
+       }
+       if p.MessageType == "" {
+               return errors.New("MessageType is required")
+       }
+       if p.Name == "" {
+               return errors.New("Name is required")
+       }
+       if p.TimestampMs <= 0 {
+               return errors.New("TimestampMs is required and must be a positive integer")
+       }
+       if p.PdpGroup == "" {
+               return errors.New("PdpGroup is required")
+       }
+       if p.PdpSubgroup == "" {
+               return errors.New("PdpSubgroup is required")
+       }
+       if p.RequestId == "" {
+               return errors.New("RequestId is required")
+       }
+
+       return nil
+}
+
+// TestPdpStateChangeSerialization_Positive tests the successful serialization of PdpStateChange.
+func TestPdpStateChangeSerialization_Success(t *testing.T) {
+       pdpStateChange := PdpStateChange{
+               Source:      "source1",
+               State:       "active",
+               MessageType: "PDP_STATE_CHANGE",
+               Name:        "ExamplePDP",
+               TimestampMs: 1633017600000,
+               PdpGroup:    "Group1",
+               PdpSubgroup: "SubGroup1",
+               RequestId:   "98765",
+       }
+
+       _, err := json.Marshal(pdpStateChange)
+       if err != nil {
+               t.Errorf("Expected no error while marshaling valid PdpStateChange, got: %v", err)
+       }
+}
+
+// TestPdpStateChangeSerialization_Negative tests the serialization of PdpStateChange with invalid fields.
+func TestPdpStateChangeSerialization_Failure(t *testing.T) {
+       pdpStateChange := PdpStateChange{
+               Source:      "",
+               State:       "",
+               MessageType: "",
+               Name:        "",
+               TimestampMs: 0,
+               PdpGroup:    "",
+               PdpSubgroup: "",
+               RequestId:   "",
+       }
+       err := pdpStateChange.Validate()
+       if err == nil {
+               t.Error("Expected an error while validating invalid PdpStatus, but got none")
+       }
+}
diff --git a/pkg/model/oapicodegen/models.go b/pkg/model/oapicodegen/models.go
new file mode 100644 (file)
index 0000000..4f1b770
--- /dev/null
@@ -0,0 +1,133 @@
+// Package api provides primitives to interact with the openapi HTTP API.
+//
+// Code generated by github.com/deepmap/oapi-codegen version v1.16.3 DO NOT EDIT.
+package oapicodegen
+
+import (
+       "time"
+
+       openapi_types "github.com/oapi-codegen/runtime/types"
+)
+
+const (
+       BasicAuthScopes = "basicAuth.Scopes"
+)
+
+// Defines values for ErrorResponseResponseCode.
+const (
+       BADGATEWAY                    ErrorResponseResponseCode = "BAD_GATEWAY"
+       BADREQUEST                    ErrorResponseResponseCode = "BAD_REQUEST"
+       CONFLICT                      ErrorResponseResponseCode = "CONFLICT"
+       EXPECTATIONFAILED             ErrorResponseResponseCode = "EXPECTATION_FAILED"
+       GATEWAYTIMEOUT                ErrorResponseResponseCode = "GATEWAY_TIMEOUT"
+       GONE                          ErrorResponseResponseCode = "GONE"
+       HTTPVERSIONNOTSUPPORTED       ErrorResponseResponseCode = "HTTP_VERSION_NOT_SUPPORTED"
+       INTERNALSERVERERROR           ErrorResponseResponseCode = "INTERNAL_SERVER_ERROR"
+       LENGTHREQUIRED                ErrorResponseResponseCode = "LENGTH_REQUIRED"
+       METHODNOTALLOWED              ErrorResponseResponseCode = "METHOD_NOT_ALLOWED"
+       NETWORKAUTHENTICATIONREQUIRED ErrorResponseResponseCode = "NETWORK_AUTHENTICATION_REQUIRED"
+       NOTACCEPTABLE                 ErrorResponseResponseCode = "NOT_ACCEPTABLE"
+       NOTIMPLEMENTED                ErrorResponseResponseCode = "NOT_IMPLEMENTED"
+       PRECONDITIONFAILED            ErrorResponseResponseCode = "PRECONDITION_FAILED"
+       PRECONDITIONREQUIRED          ErrorResponseResponseCode = "PRECONDITION_REQUIRED"
+       REQUESTEDRANGENOTSATISFIABLE  ErrorResponseResponseCode = "REQUESTED_RANGE_NOT_SATISFIABLE"
+       REQUESTENTITYTOOLARGE         ErrorResponseResponseCode = "REQUEST_ENTITY_TOO_LARGE"
+       REQUESTHEADERFIELDSTOOLARGE   ErrorResponseResponseCode = "REQUEST_HEADER_FIELDS_TOO_LARGE"
+       REQUESTTIMEOUT                ErrorResponseResponseCode = "REQUEST_TIMEOUT"
+       REQUESTURITOOLONG             ErrorResponseResponseCode = "REQUEST_URI_TOO_LONG"
+       SERVICEUNAVAILABLE            ErrorResponseResponseCode = "SERVICE_UNAVAILABLE"
+       TOOMANYREQUESTS               ErrorResponseResponseCode = "TOO_MANY_REQUESTS"
+       UNAUTHORIZED                  ErrorResponseResponseCode = "UNAUTHORIZED"
+       UNSUPPORTEDMEDIATYPE          ErrorResponseResponseCode = "UNSUPPORTED_MEDIA_TYPE"
+)
+
+// Defines values for OPADecisionResponseDecision.
+const (
+       DENY          OPADecisionResponseDecision = "DENY"
+       INDETERMINATE OPADecisionResponseDecision = "INDETERMINATE"
+       PERMIT        OPADecisionResponseDecision = "PERMIT"
+)
+
+// ErrorResponse defines model for ErrorResponse.
+type ErrorResponse struct {
+       ErrorDetails *[]string                  `json:"errorDetails,omitempty"`
+       ErrorMessage *string                    `json:"errorMessage,omitempty"`
+       PolicyName   *string                    `json:"policyName,omitempty"`
+       ResponseCode *ErrorResponseResponseCode `json:"responseCode,omitempty"`
+}
+
+// ErrorResponseResponseCode defines model for ErrorResponse.ResponseCode.
+type ErrorResponseResponseCode string
+
+// HealthCheckReport defines model for HealthCheckReport.
+type HealthCheckReport struct {
+       Code    *int32  `json:"code,omitempty"`
+       Healthy *bool   `json:"healthy,omitempty"`
+       Message *string `json:"message,omitempty"`
+       Name    *string `json:"name,omitempty"`
+       Url     *string `json:"url,omitempty"`
+}
+
+// OPADecisionRequest defines model for OPADecisionRequest.
+type OPADecisionRequest struct {
+       CurrentDate     *openapi_types.Date     `json:"currentDate,omitempty"`
+       CurrentDateTime *time.Time              `json:"currentDateTime,omitempty"`
+       CurrentTime     *time.Time              `json:"currentTime,omitempty"`
+       Input           *map[string]interface{} `json:"input,omitempty"`
+       OnapComponent   *string                 `json:"onapComponent,omitempty"`
+       OnapInstance    *string                 `json:"onapInstance,omitempty"`
+       OnapName        *string                 `json:"onapName,omitempty"`
+       PolicyName      *string                 `json:"policyName,omitempty"`
+
+       // TimeOffset Time offset in hours and minutes, e.g., '+02:00' or '-05:00'
+       TimeOffset *string `json:"timeOffset,omitempty"`
+
+       // TimeZone Timezone in IANA format (e.g., 'America/NewYork', 'Europe/Paris', 'UTC')
+       TimeZone *string `json:"timeZone,omitempty"`
+}
+
+// OPADecisionResponse defines model for OPADecisionResponse.
+type OPADecisionResponse struct {
+       Decision      *OPADecisionResponseDecision `json:"decision,omitempty"`
+       PolicyName    *string                      `json:"policyName,omitempty"`
+       StatusMessage *string                      `json:"statusMessage,omitempty"`
+}
+
+// OPADecisionResponseDecision defines model for OPADecisionResponse.Decision.
+type OPADecisionResponseDecision string
+
+// StatisticsReport defines model for StatisticsReport.
+type StatisticsReport struct {
+       Code                        *int32 `json:"code,omitempty"`
+       DenyDecisionsCount          *int64 `json:"denyDecisionsCount,omitempty"`
+       DeployFailureCount          *int64 `json:"deployFailureCount,omitempty"`
+       DeploySuccessCount          *int64 `json:"deploySuccessCount,omitempty"`
+       IndeterminantDecisionsCount *int64 `json:"indeterminantDecisionsCount,omitempty"`
+       PermitDecisionsCount        *int64 `json:"permitDecisionsCount,omitempty"`
+       TotalErrorCount             *int64 `json:"totalErrorCount,omitempty"`
+       TotalPoliciesCount          *int64 `json:"totalPoliciesCount,omitempty"`
+       TotalPolicyTypesCount       *int64 `json:"totalPolicyTypesCount,omitempty"`
+       UndeployFailureCount        *int64 `json:"undeployFailureCount,omitempty"`
+       UndeploySuccessCount        *int64 `json:"undeploySuccessCount,omitempty"`
+}
+
+// DecisionParams defines parameters for Decision.
+type DecisionParams struct {
+       // XONAPRequestID RequestID for http transaction
+       XONAPRequestID *openapi_types.UUID `json:"X-ONAP-RequestID,omitempty"`
+}
+
+// HealthcheckParams defines parameters for Healthcheck.
+type HealthcheckParams struct {
+       // XONAPRequestID RequestID for http transaction
+       XONAPRequestID *openapi_types.UUID `json:"X-ONAP-RequestID,omitempty"`
+}
+
+// StatisticsParams defines parameters for Statistics.
+type StatisticsParams struct {
+       // XONAPRequestID RequestID for http transaction
+       XONAPRequestID *openapi_types.UUID `json:"X-ONAP-RequestID,omitempty"`
+}
+
+// DecisionJSONRequestBody defines body for Decision for application/json ContentType.
+type DecisionJSONRequestBody = OPADecisionRequest
diff --git a/pkg/model/pdphealthstatus.go b/pkg/model/pdphealthstatus.go
new file mode 100644 (file)
index 0000000..387a1e8
--- /dev/null
@@ -0,0 +1,57 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// the possible values for health status of PDP.
+// https://github.com/onap/policy-models/blob/master/models-pdp
+// models-pdp/src/main/java/org/onap/policy/models/pdp/enums/PdpHealthStatus.java
+package model
+
+import (
+       "encoding/json"
+       "fmt"
+)
+
+// PdpHealthStatus represents the possible values for the health status of PDP.
+type PdpHealthStatus int
+
+// Enumerate the possible PDP health statuses
+const (
+       Healthy PdpHealthStatus = iota
+       NotHealthy
+       TestInProgress
+       Unknown
+)
+
+// String representation of PdpHealthStatus
+func (status PdpHealthStatus) String() string {
+       switch status {
+       case Healthy:
+               return "HEALTHY"
+       case NotHealthy:
+               return "NOT_HEALTHY"
+       case TestInProgress:
+               return "TEST_IN_PROGRESS"
+       case Unknown:
+               return "UNKNOWN"
+       default:
+               return fmt.Sprintf("Unknown PdpHealthStatus: %d", status)
+       }
+}
+
+func (p PdpHealthStatus) MarshalJSON() ([]byte, error) {
+       return json.Marshal(p.String())
+}
diff --git a/pkg/model/pdphealthstatus_test.go b/pkg/model/pdphealthstatus_test.go
new file mode 100644 (file)
index 0000000..0cb89cf
--- /dev/null
@@ -0,0 +1,92 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package model
+
+import (
+       "encoding/json"
+       "testing"
+)
+
+// Positive test for the string representation of valid PdpHealthStatus values
+func TestPdpHealthStatus_String_Success(t *testing.T) {
+       tests := []struct {
+               status   PdpHealthStatus
+               expected string
+       }{
+               {Healthy, "HEALTHY"},
+               {NotHealthy, "NOT_HEALTHY"},
+               {TestInProgress, "TEST_IN_PROGRESS"},
+               {Unknown, "UNKNOWN"},
+       }
+
+       for _, test := range tests {
+               if got := test.status.String(); got != test.expected {
+                       t.Errorf("PdpHealthStatus.String() = %v, want %v", got, test.expected)
+               }
+       }
+}
+
+// Negative test for the string representation of an invalid PdpHealthStatus value
+func TestPdpHealthStatus_String_Failure(t *testing.T) {
+       invalidStatus := PdpHealthStatus(100)
+       expected := "Unknown PdpHealthStatus: 100"
+
+       if got := invalidStatus.String(); got != expected {
+               t.Errorf("PdpHealthStatus.String() = %v, want %v", got, expected)
+       }
+}
+
+// Positive test for JSON marshaling of valid PdpHealthStatus values
+func TestPdpHealthStatus_MarshalJSON_Success(t *testing.T) {
+       tests := []struct {
+               status   PdpHealthStatus
+               expected string
+       }{
+               {Healthy, `"HEALTHY"`},
+               {NotHealthy, `"NOT_HEALTHY"`},
+               {TestInProgress, `"TEST_IN_PROGRESS"`},
+               {Unknown, `"UNKNOWN"`},
+       }
+
+       for _, test := range tests {
+               got, err := json.Marshal(test.status)
+               if err != nil {
+                       t.Errorf("json.Marshal() error = %v", err)
+               }
+
+               if string(got) != test.expected {
+                       t.Errorf("json.Marshal() = %v, want %v", string(got), test.expected)
+               }
+       }
+}
+
+// Negative test for JSON marshaling of an invalid PdpHealthStatus value
+func TestPdpHealthStatus_MarshalJSON_Failure(t *testing.T) {
+       invalidStatus := PdpHealthStatus(100)
+       expected := `"Unknown PdpHealthStatus: 100"`
+
+       got, err := json.Marshal(invalidStatus)
+       if err != nil {
+               t.Errorf("json.Marshal() unexpected error = %v", err)
+       }
+
+       if string(got) != expected {
+               t.Errorf("json.Marshal() = %v, want %v", string(got), expected)
+       }
+}
diff --git a/pkg/model/pdpresponsedetails.go b/pkg/model/pdpresponsedetails.go
new file mode 100644 (file)
index 0000000..8febae5
--- /dev/null
@@ -0,0 +1,34 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// represent PDP response details.
+// https://github.com/onap/policy-models/blob/master/models-pdp
+// models-pdp/src/main/java/org/onap/policy/models/pdp/concepts/PdpResponseDetails.java
+package model
+
+type PdpResponseStatus string
+
+const (
+       Success PdpResponseStatus = "SUCCESS"
+       Failure PdpResponseStatus = "FAILURE"
+)
+
+type PdpResponseDetails struct {
+       ResponseTo      *string            `json:"responseTo"`
+       ResponseStatus  *PdpResponseStatus `json:"responseStatus"`
+       ResponseMessage *string            `json:"responseMessage"`
+}
diff --git a/pkg/model/pdpresponsedetails_test.go b/pkg/model/pdpresponsedetails_test.go
new file mode 100644 (file)
index 0000000..14b9cd8
--- /dev/null
@@ -0,0 +1,89 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package model
+
+import (
+       "encoding/json"
+       "testing"
+)
+
+// Positive test for JSON marshaling of PdpResponseDetails with all fields populated
+func TestPdpResponseDetails_MarshalJSON_Success(t *testing.T) {
+       responseTo := "requestID123"
+       responseMessage := "Operation completed successfully"
+       responseStatus := Success
+
+       details := PdpResponseDetails{
+               ResponseTo:      &responseTo,
+               ResponseStatus:  &responseStatus,
+               ResponseMessage: &responseMessage,
+       }
+
+       expectedJSON := `{"responseTo":"requestID123","responseStatus":"SUCCESS","responseMessage":"Operation completed successfully"}`
+       got, err := json.Marshal(details)
+       if err != nil {
+               t.Errorf("json.Marshal() error = %v", err)
+       }
+
+       if string(got) != expectedJSON {
+               t.Errorf("json.Marshal() = %v, want %v", string(got), expectedJSON)
+       }
+}
+
+// Negative test for JSON marshaling of PdpResponseDetails with nil fields
+func TestPdpResponseDetails_MarshalJSON_Failure(t *testing.T) {
+       details := PdpResponseDetails{}
+
+       expectedJSON := `{"responseTo":null,"responseStatus":null,"responseMessage":null}`
+       got, err := json.Marshal(details)
+       if err != nil {
+               t.Errorf("json.Marshal() error = %v", err)
+       }
+
+       if string(got) != expectedJSON {
+               t.Errorf("json.Marshal() = %v, want %v", string(got), expectedJSON)
+       }
+}
+
+// Positive test for PdpResponseStatus constants
+func TestPdpResponseStatus_Success(t *testing.T) {
+       tests := []struct {
+               status   PdpResponseStatus
+               expected string
+       }{
+               {Success, "SUCCESS"},
+               {Failure, "FAILURE"},
+       }
+
+       for _, test := range tests {
+               if string(test.status) != test.expected {
+                       t.Errorf("PdpResponseStatus = %v, want %v", test.status, test.expected)
+               }
+       }
+}
+
+// Negative test for invalid PdpResponseStatus
+func TestPdpResponseStatus_Failure(t *testing.T) {
+       invalidStatus := PdpResponseStatus("INVALID")
+       expected := "INVALID"
+
+       if string(invalidStatus) != expected {
+               t.Errorf("PdpResponseStatus = %v, want %v", invalidStatus, expected)
+       }
+}
diff --git a/pkg/model/pdpstate.go b/pkg/model/pdpstate.go
new file mode 100644 (file)
index 0000000..2b54d16
--- /dev/null
@@ -0,0 +1,77 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// hold the possible values for state of PDP.
+// https://github.com/onap/policy-models/blob/master/models-pdp
+// models-pdp/src/main/java/org/onap/policy/models/pdp/enums/PdpState.java
+package model
+
+import (
+       "encoding/json"
+       "fmt"
+)
+
+// PdpState represents the possible values for the state of PDP.
+type PdpState int
+
+// Enumerate the possible PDP states
+const (
+       Passive PdpState = iota
+       Safe
+       Test
+       Active
+       Terminated
+)
+
+// String representation of PdpState
+func (state PdpState) String() string {
+       switch state {
+       case Passive:
+               return "PASSIVE"
+       case Safe:
+               return "SAFE"
+       case Test:
+               return "TEST"
+       case Active:
+               return "ACTIVE"
+       case Terminated:
+               return "TERMINATED"
+       default:
+               return fmt.Sprintf("Unknown PdpState: %d", state)
+       }
+}
+
+func (s PdpState) MarshalJSON() ([]byte, error) {
+       return json.Marshal(s.String())
+}
+
+func ConvertStringToEnumState(state string) (PdpState, error) {
+       switch state {
+       case "PASSIVE":
+               return Passive, nil
+       case "SAFE":
+               return Safe, nil
+       case "TEST":
+               return Test, nil
+       case "ACTIVE":
+               return Active, nil
+       case "TERMINATED":
+               return Terminated, nil
+       default:
+               return -1, fmt.Errorf("Unknown PdpState: %s", state)
+       }
+}
diff --git a/pkg/model/pdpstate_test.go b/pkg/model/pdpstate_test.go
new file mode 100644 (file)
index 0000000..35ff6af
--- /dev/null
@@ -0,0 +1,128 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package model
+
+import (
+       "encoding/json"
+       "testing"
+)
+
+// Positive test cases for PdpState.String
+func TestPdpState_String_Success(t *testing.T) {
+       tests := []struct {
+               state    PdpState
+               expected string
+       }{
+               {Passive, "PASSIVE"},
+               {Safe, "SAFE"},
+               {Test, "TEST"},
+               {Active, "ACTIVE"},
+               {Terminated, "TERMINATED"},
+       }
+
+       for _, test := range tests {
+               got := test.state.String()
+               if got != test.expected {
+                       t.Errorf("PdpState.String() = %v, want %v", got, test.expected)
+               }
+       }
+}
+
+// Negative test case for PdpState.String
+func TestPdpState_String_Failure(t *testing.T) {
+       state := PdpState(100) // Unknown state
+       expected := "Unknown PdpState: 100"
+       got := state.String()
+       if got != expected {
+               t.Errorf("PdpState.String() = %v, want %v", got, expected)
+       }
+}
+
+// Positive test cases for PdpState.MarshalJSON
+func TestPdpState_MarshalJSON_Success(t *testing.T) {
+       tests := []struct {
+               state    PdpState
+               expected string
+       }{
+               {Passive, `"PASSIVE"`},
+               {Safe, `"SAFE"`},
+               {Test, `"TEST"`},
+               {Active, `"ACTIVE"`},
+               {Terminated, `"TERMINATED"`},
+       }
+
+       for _, test := range tests {
+               got, err := json.Marshal(test.state)
+               if err != nil {
+                       t.Errorf("json.Marshal() error = %v", err)
+                       continue
+               }
+
+               if string(got) != test.expected {
+                       t.Errorf("json.Marshal() = %v, want %v", string(got), test.expected)
+               }
+       }
+}
+
+// Negative test case for PdpState.MarshalJSON
+func TestPdpState_MarshalJSON_Failure(t *testing.T) {
+       state := PdpState(100) // Unknown state
+       expected := `"Unknown PdpState: 100"`
+
+       got, err := json.Marshal(state)
+       if err != nil {
+               t.Errorf("json.Marshal() error = %v", err)
+       } else if string(got) != expected {
+               t.Errorf("json.Marshal() = %v, want %v", string(got), expected)
+       }
+}
+
+// Positive test cases for ConvertStringToEnumState
+func TestConvertStringToEnumState_Success(t *testing.T) {
+       tests := []struct {
+               input    string
+               expected PdpState
+       }{
+               {"PASSIVE", Passive},
+               {"SAFE", Safe},
+               {"TEST", Test},
+               {"ACTIVE", Active},
+               {"TERMINATED", Terminated},
+       }
+
+       for _, test := range tests {
+               got, err := ConvertStringToEnumState(test.input)
+               if err != nil {
+                       t.Errorf("ConvertStringToEnumState(%v) unexpected error = %v", test.input, err)
+                       continue
+               }
+               if got != test.expected {
+                       t.Errorf("ConvertStringToEnumState(%v) = %v, want %v", test.input, got, test.expected)
+               }
+       }
+}
+
+// Negative test case for ConvertStringToEnumState
+func TestConvertStringToEnumState_Failure(t *testing.T) {
+       input := "UNKNOWN" // Invalid state
+       _, err := ConvertStringToEnumState(input)
+       if err == nil {
+               t.Errorf("ConvertStringToEnumState(%v) expected error, got nil", input)
+       }
+}
diff --git a/pkg/model/toscaconceptidentifier.go b/pkg/model/toscaconceptidentifier.go
new file mode 100644 (file)
index 0000000..7afc7b1
--- /dev/null
@@ -0,0 +1,56 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Identifies a concept. Both the name and version must be non-null.
+// https://github.com/onap/policy-models/blob/master/models-tosca
+// models-tosca/src/main/java/org/onap/policy/models/tosca/authorative/concepts/ToscaConceptIdentifier.java
+package model
+
+import (
+       "fmt"
+)
+
+type ToscaConceptIdentifier struct {
+       Name    string
+       Version string
+}
+
+func NewToscaConceptIdentifier(name, version string) *ToscaConceptIdentifier {
+       return &ToscaConceptIdentifier{
+               Name:    name,
+               Version: version,
+       }
+}
+
+func NewToscaConceptIdentifierFromKey(key PfKey) *ToscaConceptIdentifier {
+       return &ToscaConceptIdentifier{
+               Name:    key.Name,
+               Version: key.Version,
+       }
+}
+
+func (id *ToscaConceptIdentifier) ValidatePapRest() error {
+       if id.Name == "" || id.Version == "" {
+               return fmt.Errorf("name and version must be non-empty")
+       }
+       return nil
+}
+
+type PfKey struct {
+       Name    string
+       Version string
+}
diff --git a/pkg/model/toscaconceptidentifier_test.go b/pkg/model/toscaconceptidentifier_test.go
new file mode 100644 (file)
index 0000000..a131483
--- /dev/null
@@ -0,0 +1,106 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package model
+
+import (
+       "testing"
+)
+
+// Positive test for NewToscaConceptIdentifier
+func TestNewToscaConceptIdentifier_Success(t *testing.T) {
+       name := "ExampleName"
+       version := "1.0.0"
+       id := NewToscaConceptIdentifier(name, version)
+
+       if id.Name != name {
+               t.Errorf("Expected Name: %s, got: %s", name, id.Name)
+       }
+       if id.Version != version {
+               t.Errorf("Expected Version: %s, got: %s", version, id.Version)
+       }
+}
+
+// Negative test for NewToscaConceptIdentifier with empty name and version
+func TestNewToscaConceptIdentifier_Failure(t *testing.T) {
+       name := ""
+       version := ""
+       id := NewToscaConceptIdentifier(name, version)
+
+       if id.Name != name {
+               t.Errorf("Expected Name to be empty, got: %s", id.Name)
+       }
+       if id.Version != version {
+               t.Errorf("Expected Version to be empty, got: %s", id.Version)
+       }
+}
+
+// Positive test for NewToscaConceptIdentifierFromKey
+func TestNewToscaConceptIdentifierFromKey_Success(t *testing.T) {
+       key := PfKey{Name: "KeyName", Version: "1.0.0"}
+       id := NewToscaConceptIdentifierFromKey(key)
+
+       if id.Name != key.Name {
+               t.Errorf("Expected Name: %s, got: %s", key.Name, id.Name)
+       }
+       if id.Version != key.Version {
+               t.Errorf("Expected Version: %s, got: %s", key.Version, id.Version)
+       }
+}
+
+// Negative test for NewToscaConceptIdentifierFromKey with empty PfKey values
+func TestNewToscaConceptIdentifierFromKey_Failure(t *testing.T) {
+       key := PfKey{Name: "", Version: ""}
+       id := NewToscaConceptIdentifierFromKey(key)
+
+       if id.Name != key.Name {
+               t.Errorf("Expected Name to be empty, got: %s", id.Name)
+       }
+       if id.Version != key.Version {
+               t.Errorf("Expected Version to be empty, got: %s", id.Version)
+       }
+}
+
+// Positive test for ToscaConceptIdentifier.ValidatePapRest
+func TestToscaConceptIdentifier_ValidatePapRest_Success(t *testing.T) {
+       id := NewToscaConceptIdentifier("ValidName", "1.0.0")
+       err := id.ValidatePapRest()
+
+       if err != nil {
+               t.Errorf("Expected no error, got: %v", err)
+       }
+}
+
+// Negative test for ToscaConceptIdentifier.ValidatePapRest with invalid values
+func TestToscaConceptIdentifier_ValidatePapRest_Failure(t *testing.T) {
+       tests := []struct {
+               id        *ToscaConceptIdentifier
+               expectErr bool
+       }{
+               {NewToscaConceptIdentifier("", "1.0"), true},       // Missing name
+               {NewToscaConceptIdentifier("ValidName", ""), true}, // Missing version
+               {NewToscaConceptIdentifier("", ""), true},          // Missing name and version
+       }
+
+       for _, test := range tests {
+               err := test.id.ValidatePapRest()
+               if (err != nil) != test.expectErr {
+                       t.Errorf("ValidatePapRest() for id: %+v, got error = %v, expectErr = %v", test.id, err != nil, test.expectErr)
+               }
+       }
+}
diff --git a/pkg/opasdk/opasdk.go b/pkg/opasdk/opasdk.go
new file mode 100644 (file)
index 0000000..da6c7cc
--- /dev/null
@@ -0,0 +1,92 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// The opasdk package provides functionalities for integrating with the Open Policy Agent
+// (OPA) SDK, including reading configurations and managing a singleton OPA instance.
+// This package is designed to ensure efficient, thread-safe initialization and configuration
+// of the OPA instance.
+package opasdk
+
+import (
+       "bytes"
+       "context"
+       "fmt"
+       "io"
+       "os"
+       "policy-opa-pdp/consts"
+       "policy-opa-pdp/pkg/log"
+       "sync"
+
+       "github.com/open-policy-agent/opa/sdk"
+)
+
+// Define the structs
+var (
+       opaInstance *sdk.OPA  //A singleton instance of the OPA object
+       once        sync.Once //A sync.Once variable used to ensure that the OPA instance is initialized only once,
+)
+
+// reads JSON configuration from a file and return a jsonReader
+func getJSONReader(filePath string, openFunc func(string) (*os.File, error),
+       readAllFunc func(io.Reader) ([]byte, error)) (*bytes.Reader, error) {
+       file, err := openFunc(filePath)
+       if err != nil {
+               return nil, fmt.Errorf("error opening file: %w", err)
+       }
+       defer file.Close()
+
+       byteValue, err := readAllFunc(file)
+       if err != nil {
+               return nil, fmt.Errorf("error reading config file: %w", err)
+       }
+
+       jsonReader := bytes.NewReader(byteValue)
+       return jsonReader, nil
+}
+
+// Returns a singleton instance of the OPA object. The initialization of the instance is
+// thread-safe, and the OPA object is configured using a JSON configuration file.
+func GetOPASingletonInstance() (*sdk.OPA, error) {
+       var err error
+       once.Do(func() {
+               var opaErr error
+               opaInstance, opaErr = sdk.New(context.Background(), sdk.Options{
+                       // Configure your OPA instance here
+                       V1Compatible: true,
+               })
+               log.Debugf("Create an instance of OPA Object")
+               if opaErr != nil {
+                       log.Warnf("Error creating OPA instance: %s", opaErr)
+                       err = opaErr
+                       return
+               } else {
+                       jsonReader, jsonErr := getJSONReader(consts.OpasdkConfigPath, os.Open, io.ReadAll)
+                       if jsonErr != nil {
+                               log.Warnf("Error getting JSON reader: %s", jsonErr)
+                               err = jsonErr
+                               return
+                       }
+                       log.Debugf("Configure an instance of OPA Object")
+
+                       opaInstance.Configure(context.Background(), sdk.ConfigOptions{
+                               Config: jsonReader,
+                       })
+               }
+       })
+
+       return opaInstance, err
+}
diff --git a/pkg/opasdk/opasdk_test.go b/pkg/opasdk/opasdk_test.go
new file mode 100644 (file)
index 0000000..b6c205b
--- /dev/null
@@ -0,0 +1,111 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package opasdk
+
+import (
+       "io"
+       "os"
+       "policy-opa-pdp/consts"
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+)
+
+func TestGetOPASingletonInstance_ConfigurationFileNotexisting(t *testing.T) {
+       consts.OpasdkConfigPath = "/app/config/config.json"
+       opaInstance, err := GetOPASingletonInstance()
+       assert.NotNil(t, err) //error no such file or directory /app/config/config.json
+       assert.NotNil(t, opaInstance)
+}
+
+func TestGetOPASingletonInstance_SingletonBehavior(t *testing.T) {
+       tmpFile, err := os.CreateTemp("", "config.json")
+       if err != nil {
+               t.Fatalf("Failed to create temp file: %v", err)
+       }
+       defer os.Remove(tmpFile.Name())
+
+       consts.OpasdkConfigPath = tmpFile.Name()
+
+       // Call the function multiple times
+       opaInstance1, err1 := GetOPASingletonInstance()
+       opaInstance2, err2 := GetOPASingletonInstance()
+
+       // Assertions
+       assert.Nil(t, err1)
+       assert.Nil(t, err2)
+       assert.NotNil(t, opaInstance1)
+       assert.NotNil(t, opaInstance2)
+       assert.Equal(t, opaInstance1, opaInstance2) // Ensure it's the same instance
+}
+
+func TestGetOPASingletonInstance_OPAInstanceCreation(t *testing.T) {
+       tmpFile, err := os.CreateTemp("", "config.json")
+       if err != nil {
+               t.Fatalf("Failed to create temp file: %v", err)
+       }
+       defer os.Remove(tmpFile.Name())
+
+       consts.OpasdkConfigPath = tmpFile.Name()
+
+       // Call the function
+       opaInstance, err := GetOPASingletonInstance()
+
+       // Assertions
+       assert.Nil(t, err)
+       assert.NotNil(t, opaInstance)
+}
+
+// Mock for os.Open
+type MockFile struct {
+       mock.Mock
+}
+
+func (m *MockFile) Open(name string) (*os.File, error) {
+       args := m.Called(name)
+       return args.Get(0).(*os.File), args.Error(1)
+}
+
+// Mock for io.ReadAll
+func mockReadAll(r io.Reader) ([]byte, error) {
+       return []byte(`{"config": "test"}`), nil
+}
+
+func TestGetJSONReader(t *testing.T) {
+       // Create a mock file
+       mockFile := new(MockFile)
+       mockFile.On("Open", "/app/config/config.json").Return(&os.File{}, nil)
+
+       // Call the function with mock functions
+       jsonReader, err := getJSONReader("/app/config/config.json", mockFile.Open, mockReadAll)
+
+       // Check the results
+       assert.NoError(t, err)
+       assert.NotNil(t, jsonReader)
+
+       // Check the content of the jsonReader
+       expectedContent := `{"config": "test"}`
+       actualContent := make([]byte, len(expectedContent))
+       jsonReader.Read(actualContent)
+       assert.Equal(t, expectedContent, string(actualContent))
+
+       // Assert that the mock methods were called
+       mockFile.AssertCalled(t, "Open", "/app/config/config.json")
+}
diff --git a/pkg/pdpattributes/pdpattributes.go b/pkg/pdpattributes/pdpattributes.go
new file mode 100644 (file)
index 0000000..70744fd
--- /dev/null
@@ -0,0 +1,63 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// The pdpattributes package provides utilities for managing and configuring attributes related to the
+// Policy Decision Point (PDP). This includes generating unique PDP names, and setting or retrieving
+// subgroup and heartbeat interval values.
+package pdpattributes
+
+import (
+       "github.com/google/uuid"
+       "policy-opa-pdp/pkg/log"
+)
+
+var (
+       PdpName              string // A unique identifier for the PDP instance
+       PdpSubgroup          string
+       PdpHeartbeatInterval int64 // The interval (in seconds) at which the PDP sends heartbeat signals
+)
+
+func init() {
+       PdpName = GenerateUniquePdpName()
+       log.Debugf("Name: %s", PdpName)
+}
+
+// Generates a unique PDP name by appending a randomly generated UUID
+func GenerateUniquePdpName() string {
+       return "opa-" + uuid.New().String()
+}
+
+// sets the Pdp Subgroup retrieved from the message from Pap
+func SetPdpSubgroup(pdpsubgroup string) {
+       PdpSubgroup = pdpsubgroup
+}
+
+// Retrieves the current PDP subgroup value.
+func GetPdpSubgroup() string {
+       return PdpSubgroup
+}
+
+// sets the PdpHeratbeatInterval retrieved from the message from Pap
+func SetPdpHeartbeatInterval(pdpHeartbeatInterval int64) {
+       PdpHeartbeatInterval = pdpHeartbeatInterval
+}
+
+// Retrieves the current PDP heartbeat interval value.
+func GetPdpHeartbeatInterval() int64 {
+       return PdpHeartbeatInterval
+
+}
diff --git a/pkg/pdpattributes/pdpattributes_test.go b/pkg/pdpattributes/pdpattributes_test.go
new file mode 100644 (file)
index 0000000..0870ed6
--- /dev/null
@@ -0,0 +1,87 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package pdpattributes
+
+import (
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+)
+
+func TestGenerateUniquePdpName_Success(t *testing.T) {
+       t.Run("GenerateValidPdpName", func(t *testing.T) {
+               pdpName := GenerateUniquePdpName()
+               assert.Contains(t, pdpName, "opa-", "Expected PDP name to start with 'opa-'")
+       })
+}
+
+func TestGenerateUniquePdpName_Failure(t *testing.T) {
+       t.Run("UniqueNamesCheck", func(t *testing.T) {
+               pdpName1 := GenerateUniquePdpName()
+               pdpName2 := GenerateUniquePdpName()
+               assert.NotEqual(t, pdpName1, pdpName2, "Expected different UUID for each generated PDP name")
+               assert.Len(t, pdpName1, len("opa-")+36, "Expected length of PDP name to match 'opa-<UUID>' format")
+       })
+}
+
+func TestSetPdpSubgroup_Success(t *testing.T) {
+       t.Run("ValidSubgroup", func(t *testing.T) {
+               expectedSubgroup := "subgroup1"
+               SetPdpSubgroup(expectedSubgroup)
+               assert.Equal(t, expectedSubgroup, GetPdpSubgroup(), "Expected PDP subgroup to match set value")
+       })
+}
+
+func TestSetPdpSubgroup_Failure(t *testing.T) {
+       t.Run("EmptySubgroup", func(t *testing.T) {
+               SetPdpSubgroup("")
+               assert.Equal(t, "", GetPdpSubgroup(), "Expected PDP subgroup to be empty when set to empty string")
+       })
+
+       t.Run("LargeSubgroup", func(t *testing.T) {
+               largeSubgroup := make([]byte, 1024*1024) // 1MB of 'a' characters
+               for i := range largeSubgroup {
+                       largeSubgroup[i] = 'a'
+               }
+               SetPdpSubgroup(string(largeSubgroup))
+               assert.Equal(t, string(largeSubgroup), GetPdpSubgroup(), "Expected large PDP subgroup to match set value")
+       })
+}
+
+func TestSetPdpHeartbeatInterval_Success(t *testing.T) {
+       t.Run("ValidHeartbeatInterval", func(t *testing.T) {
+               expectedInterval := int64(30)
+               SetPdpHeartbeatInterval(expectedInterval)
+               assert.Equal(t, expectedInterval, GetPdpHeartbeatInterval(), "Expected heartbeat interval to match set value")
+       })
+}
+
+func TestSetPdpHeartbeatInterval_Failure(t *testing.T) {
+       t.Run("FailureHeartbeatInterval", func(t *testing.T) {
+               SetPdpHeartbeatInterval(-10)
+               assert.Equal(t, int64(-10), GetPdpHeartbeatInterval(), "Expected heartbeat interval to handle negative values")
+       })
+
+       t.Run("LargeHeartbeatInterval", func(t *testing.T) {
+               largeInterval := int64(time.Hour * 24 * 365 * 10) // 10 years in seconds
+               SetPdpHeartbeatInterval(largeInterval)
+               assert.Equal(t, largeInterval, GetPdpHeartbeatInterval(), "Expected PDP heartbeat interval to handle large values")
+       })
+}
diff --git a/pkg/pdpstate/pdpstate.go b/pkg/pdpstate/pdpstate.go
new file mode 100644 (file)
index 0000000..0adaa2e
--- /dev/null
@@ -0,0 +1,45 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// The pdpstate package manages the state of the Policy Decision Point (PDP), allowing for dynamic updates
+// and retrieval of the PDP's current operational state. States are represented using the model.PdpState type.
+package pdpstate
+
+import (
+       "policy-opa-pdp/pkg/model"
+)
+
+var (
+       State           model.PdpState = model.Passive // The current state of the PDP.
+       GetCurrentState                = GetState      // An alias for the GetState function.
+)
+
+// sets the Pdp State retrieved from the message from Pap
+func SetState(stringState string) error {
+       newState, err := model.ConvertStringToEnumState(stringState)
+       if err != nil {
+               return err
+       }
+
+       State = newState
+       return nil
+}
+
+// Retrieves the current PDP state.
+func GetState() model.PdpState {
+       return State
+}
diff --git a/pkg/pdpstate/pdpstate_test.go b/pkg/pdpstate/pdpstate_test.go
new file mode 100644 (file)
index 0000000..6b7078c
--- /dev/null
@@ -0,0 +1,43 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+package pdpstate
+
+import (
+       "testing"
+
+       "github.com/stretchr/testify/assert"
+       "policy-opa-pdp/pkg/model"
+)
+
+func TestSetState_Success(t *testing.T) {
+       t.Run("ValidState", func(t *testing.T) {
+               err := SetState("ACTIVE")
+               assert.NoError(t, err, "Expected no error for valid state")
+               assert.Equal(t, model.Active, GetState(), "Expected state to be set to Active")
+       })
+}
+
+func TestSetState_Failure(t *testing.T) {
+       State = model.Passive
+       t.Run("InvalidState", func(t *testing.T) {
+               err := SetState("InvalidState")
+               assert.Error(t, err, "Expected an error for invalid state")
+               assert.Equal(t, model.Passive, GetState(), "Expected state to remain unchanged when setting invalid state")
+       })
+}
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
new file mode 100644 (file)
index 0000000..c2cb591
--- /dev/null
@@ -0,0 +1,30 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+// Package utils provides common  functionalities
+
+package utils
+
+import (
+       "github.com/google/uuid"
+)
+
+// validates if the given request is in valid uuid form
+func IsValidUUID(u string) bool {
+       _, err := uuid.Parse(u)
+       return err == nil
+}
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
new file mode 100644 (file)
index 0000000..b70fa2b
--- /dev/null
@@ -0,0 +1,59 @@
+// -
+//   ========================LICENSE_START=================================
+//   Copyright (C) 2024: Deutsche Telecom
+//
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+
+package utils
+
+import (
+       "github.com/google/uuid"
+       "testing"
+)
+
+// Positive Test Case: Valid UUIDs
+func TestIsValidUUIDPositive(t *testing.T) {
+       // Define valid UUID strings
+       validUUIDs := []string{
+               "123e4567-e89b-12d3-a456-426614174000", // Standard UUID
+               uuid.New().String(),                    // Dynamically generated UUID
+       }
+
+       for _, u := range validUUIDs {
+               t.Run("Valid UUID", func(t *testing.T) {
+                       if !IsValidUUID(u) {
+                               t.Errorf("Expected valid UUID, but got invalid for %s", u)
+                       }
+               })
+       }
+}
+
+// Negative Test Case: Invalid UUIDs
+func TestIsValidUUIDNegative(t *testing.T) {
+       // Define invalid UUID strings
+       invalidUUIDs := []string{
+               "123e4567-e89b-12d3-a456-42661417400",  // Invalid: missing character at the end
+               "invalid-uuid-format",                  // Invalid: incorrect format
+               "123e4567-e89b-12d3-a456-42661417400x", // Invalid: contains extra non-hex character
+               " ",                                    // Invalid: empty string
+       }
+
+       for _, u := range invalidUUIDs {
+               t.Run("Invalid UUID", func(t *testing.T) {
+                       if IsValidUUID(u) {
+                               t.Errorf("Expected invalid UUID, but got valid for %s", u)
+                       }
+               })
+       }
+}
diff --git a/test/Opagroup.json b/test/Opagroup.json
new file mode 100644 (file)
index 0000000..002b962
--- /dev/null
@@ -0,0 +1,23 @@
+{
+    "groups": [
+        {
+            "name": "defaultGroup",
+            "pdpGroupState": "ACTIVE",
+            "properties": {},
+            "pdpSubgroups": [
+                {
+                    "pdpType": "opa",
+                    "desiredInstanceCount": 1,
+                    "properties": {},
+                    "supportedPolicyTypes": [
+                        {
+                            "name": "onap.policies.native.opa",
+                            "version": "1.0.0"
+                        }
+                    ],
+                    "policies": []
+                }
+            ]
+        }
+    ]
+}
diff --git a/test/README.md b/test/README.md
new file mode 100644 (file)
index 0000000..7940342
--- /dev/null
@@ -0,0 +1,54 @@
+# Testing OPA
+
+## Verification API Calls
+
+curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS","currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC", "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z","policyName":"example/allow","input":{"method":"POST","path":["users"]}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision
+
+curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS","currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC", "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z", "policyName":"role/allow","input":{"user":"alice","action":"write","object":"id123","type":"dog"}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision
+
+## PERMIT for policy:action
+
+curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS", "currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC", "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z","policyName":"action/allow","input":{"user":"alice","action":"delete","type":"server"}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision
+
+{"decision":"PERMIT","policyName":"action/allow","statusMessage":"OPA Allowed"}
+
+## DENY for policy:action
+
+curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS", "currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC", "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z","policyName":"action/allow","input":{"user":"charlie","action":"delete","type":"server"}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision
+
+{"decision":"DENY","policyName":"action/allow","statusMessage":"OPA Denied"}
+
+## PERMIT for policy:account
+
+curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS", "currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC","timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z","policyName":"account/allow", "input":{"creditor_account":11111,"creditor":"alice","debtor_account":22222,"debtor":"bob","period":30,"amount":1000}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision
+
+{"decision":"PERMIT","policyName":"account/allow","statusMessage":"OPA Allowed"}
+
+## DENY for policy:account
+
+curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS", "currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC", "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z","policyName":"account/allow", "input":{"creditor_account":11111,"creditor":"alice","debtor_account":22222,"debtor":"bob","period":31,"amount":1000}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision
+
+{"decision":"DENY","policyName":"account/allow","statusMessage":"OPA Denied"}
+
+## PERMIT for policy:organization
+
+curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS", "currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC", "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z","policyName":"organization/allow", "input":{"user":"alice","action": "read","component": "component_A","project": "project_A", "organization": "org_A"}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision
+{"decision":"PERMIT","policyName":"organization/allow","statusMessage":"OPA Allowed"}
+
+## DENY for policy:organization
+
+curl -u 'policyadmin:zb!XztG34' -H 'Content-Type: application/json' -H 'Accept: application/json' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -d '{"onapName":"CDS","onapComponent":"CDS","onapInstance":"CDS", "currentDate": "2024-11-22", "currentTime": "2024-11-22T11:34:56Z", "timeZone": "UTC", "timeOffset": "+05:30", "currentDateTime": "2024-11-22T12:08:00Z","policyName":"organization/allow", "input":{"user":"charlie","action": "edit","component": "component_A","project": "project_A", "organization": "org_A"}}' -X POST http://0.0.0.0:8282/policy/pdpx/v1/decision
+
+{"decision":"DENY","policyName":"organization/allow","statusMessage":"OPA Denied"}
+
+## HealthCheck API Call With Response
+
+curl -u 'policyadmin:zb!XztG34' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -X GET http://0.0.0.0:8282/policy/pdpx/v1/healthcheck
+
+{"code":200,"healthy":true,"message":"alive","name":"opa-9f0248ea-807e-45f6-8e0f-935e570b75cc","url":"self"}
+
+## Statistics API Call With Response
+
+curl -u 'policyadmin:zb!XztG34' --header 'X-ONAP-RequestID:8e6f784e-c9cb-42f6-bcc9-edb5d0af1ce1' -X GET http://0.0.0.0:8282/policy/pdpx/v1/statistics
+
+{"code":200,"denyDecisionsCount":10,"deployFailureCount":0,"deploySuccessCount":0,"indeterminantDecisionsCount":0,"permitDecisionsCount":18,"totalErrorCount":4,"totalPoliciesCount":0,"totalPolicyTypesCount":1,"undeployFailureCount":0,"undeploySuccessCount":0}
diff --git a/test/config.json b/test/config.json
new file mode 100644 (file)
index 0000000..3f2aa43
--- /dev/null
@@ -0,0 +1,24 @@
+{
+  "logging": {
+    "level": "debug"
+  },
+  "services": [
+    {
+      "name": "opa-bundle-server",
+      "url": "http://localhost:8282/opa/bundles"
+    }
+  ],
+  "bundles": {
+    "opabundle": {
+      "service": "opa-bundle-server",
+      "resource": "bundle.tar.gz",
+      "polling": {
+        "min_delay_seconds": 60,
+        "max_delay_seconds": 120
+      }
+    }
+  },
+  "decision_logs": {
+    "console": true
+  }
+}
diff --git a/test/config/opa-pdp/config.json b/test/config/opa-pdp/config.json
new file mode 100644 (file)
index 0000000..3f2aa43
--- /dev/null
@@ -0,0 +1,24 @@
+{
+  "logging": {
+    "level": "debug"
+  },
+  "services": [
+    {
+      "name": "opa-bundle-server",
+      "url": "http://localhost:8282/opa/bundles"
+    }
+  ],
+  "bundles": {
+    "opabundle": {
+      "service": "opa-bundle-server",
+      "resource": "bundle.tar.gz",
+      "polling": {
+        "min_delay_seconds": 60,
+        "max_delay_seconds": 120
+      }
+    }
+  },
+  "decision_logs": {
+    "console": true
+  }
+}
diff --git a/test/config/opa-pdp/groups.json b/test/config/opa-pdp/groups.json
new file mode 100644 (file)
index 0000000..502700c
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "groups": [
+    {
+      "name": "defaultGroup",
+      "version": "1.0.0",
+      "description": "The default group that registers all supported policy types and pdps.",
+      "pdpGroupState": "ACTIVE",
+      "pdpSubgroups": [
+        {
+          "pdpType": "opa"
+        }
+      ]
+    }
+  ]
+}
diff --git a/test/config/opa-pdp/policy-opa-pdp.sh b/test/config/opa-pdp/policy-opa-pdp.sh
new file mode 100755 (executable)
index 0000000..7ed14cb
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+
+#Creation of Policies and Groups
+sh scripts.sh
+
+
+#Execution of OPA-PDP bin
+/app/opa-pdp
diff --git a/test/docker-compose.yml b/test/docker-compose.yml
new file mode 100644 (file)
index 0000000..6778882
--- /dev/null
@@ -0,0 +1,143 @@
+version: '3.8'
+services:
+   mariadb:
+      image: nexus3.onap.org:10001/mariadb:10.10.2
+      container_name: mariadb
+      hostname: mariadb
+      command: ['--lower-case-table-names=1', '--wait_timeout=28800', '--default-authentication-plugin=mysql_native_password']
+      env_file: ./config/db/db.conf
+      volumes:
+         - ./config/db:/docker-entrypoint-initdb.d
+         - ./config/clamp/policy-clamp-create-tables.sql:/tmp/policy-clamp-create-tables.sql
+      ports:
+       - "3306:3306"
+   policy-db-migrator:
+      image: nexus3.onap.org:10001/onap/policy-db-migrator:4.0.1-SNAPSHOT
+      container_name: policy-db-migrator
+      hostname: policy-db-migrator
+      depends_on:
+        - mariadb
+      expose:
+       - 6824
+      env_file: ./config/db/db.conf
+      environment:
+        SQL_DB: policyadmin
+        SQL_HOST: mariadb
+      volumes:
+         - ./config/db-migrator/init.sh:/opt/app/policy/bin/db_migrator_policy_init.sh:ro
+         - ./wait_for_port.sh:/tmp/wait_for_port.sh
+      entrypoint: sh /tmp/wait_for_port.sh
+      command: [
+              '-c',
+                '/opt/app/policy/bin/db_migrator_policy_init.sh',
+                'mariadb', '3306'
+               ]
+   api:
+      image: nexus3.onap.org:10001/onap/policy-api:4.0.1-SNAPSHOT
+      container_name: policy-api
+      depends_on:
+       - policy-db-migrator
+      hostname: policy-api
+      ports:
+       - 30002:6969
+      volumes:
+       - ./config/api/apiParameters.yaml:/opt/app/policy/api/etc/apiParameters.yaml:ro
+       - ./config/api/logback.xml:/opt/app/policy/api/etc/logback.xml:ro
+       - ./wait_for_port.sh:/opt/app/policy/api/bin/wait_for_port.sh
+      entrypoint: sh wait_for_port.sh
+      command: [
+        '-c', './policy-api.sh',
+        'mariadb', '3306',
+        'policy-db-migrator', '6824'
+        ]
+   pap:
+      image: nexus3.onap.org:10001/onap/policy-pap:4.0.1-SNAPSHOT
+      container_name: policy-pap
+      depends_on:
+       - mariadb
+       - kafka
+       - api
+      hostname: policy-pap
+      ports:
+       - 30003:6969
+      volumes:
+       - ./config/pap/papParameters.yaml:/opt/app/policy/pap/etc/papParameters.yaml:ro
+       - ./config/pap/groups.json:/opt/app/policy/pap/etc/mounted/groups.json:ro
+       - ./config/pap/logback.xml:/opt/app/policy/pap/etc/logback.xml:ro
+       - ./wait_for_port.sh:/opt/app/policy/pap/bin/wait_for_port.sh
+      entrypoint: sh wait_for_port.sh
+      command: [
+        '-c', './policy-pap.sh',
+        'mariadb', '3306',
+        'kafka', '9092',
+        'api', '6969'
+        ]
+   zookeeper:
+     image: confluentinc/cp-zookeeper:latest
+     environment:
+       ZOOKEEPER_CLIENT_PORT: 2181
+       ZOOKEEPER_TICK_TIME: 2000
+     ports:
+       - 2181:2181
+
+   pdp:
+      image: docker.io/opa-pdp:1.0.0
+      container_name: opa-pdp
+      depends_on:
+       - mariadb
+       - kafka
+       - api
+       - pap
+      hostname: opa-pdp
+      volumes:
+       - ./config/opa-pdp/config.json:/app/config/config.json:ro
+       - ./config/opa-pdp/groups.json:/app/groups.json:ro
+       - ./config/opa-pdp/policy-opa-pdp.sh:/app/policy-opa-pdp.sh:ro
+       - ./wait_for_port.sh:/app/wait_for_port.sh
+       - ./scripts.sh:/app/scripts.sh
+       - ./Opagroup.json:/app/Opagroup.json
+       - ./policy-new.yaml:/app/policy-new.yaml
+       - type: bind
+         source: ./policies
+         target: /app/policies 
+
+      environment:
+        LOG_LEVEL: debug
+        KAFKA_URL: "kafka:9092"
+        PAP_TOPIC: policy-pdp-pap
+        GROUPID: opa-pdp
+        API_USER: policyadmin
+        API_PASSWORD: "zb!XztG34"
+      entrypoint: sh wait_for_port.sh
+      command: [
+        '-c', './policy-opa-pdp.sh',
+        'mariadb', '3306',
+        'kafka', '9092',
+        'api', '6969',
+        'pap', '6969'
+        ]
+      ports:
+        - 8282:8282
+   zookeeper:
+     image: confluentinc/cp-zookeeper:latest
+     environment:
+       ZOOKEEPER_CLIENT_PORT: 2181
+       ZOOKEEPER_TICK_TIME: 2000
+     ports:
+       - 2181:2181
+   
+   kafka:
+     image: confluentinc/cp-kafka:latest
+     container_name: kafka
+     depends_on:
+       - zookeeper
+     ports:
+       - 29092:29092
+       - 9092:9092
+     environment:
+       KAFKA_BROKER_ID: 1
+       KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+       KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
+       KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
+       KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
+       KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
diff --git a/test/policies/abac/policy.rego b/test/policies/abac/policy.rego
new file mode 100644 (file)
index 0000000..9dc6ea9
--- /dev/null
@@ -0,0 +1,20 @@
+package abac
+
+import rego.v1
+
+default allow := false
+
+allow if {
+ viewable_sensor_data
+ action_is_read
+}
+
+action_is_read if "read" in input.actions
+
+viewable_sensor_data contains view_data if {
+ some sensor_data in data.abac.sensor_data
+ sensor_data.timestamp >= input.time_period.from
+ sensor_data.timestamp < input.time_period.to
+
+ view_data := {datatype: sensor_data[datatype] | datatype in input.datatypes}
+}
diff --git a/test/policies/account/policy.rego b/test/policies/account/policy.rego
new file mode 100644 (file)
index 0000000..f99e8eb
--- /dev/null
@@ -0,0 +1,17 @@
+package account
+
+import rego.v1
+
+default allow := false
+
+allow if {
+ creditor_is_valid
+ debtor_is_valid
+ period_is_valid
+ amount_is_valid
+}
+creditor_is_valid if data.account.account_attributes[input.creditor_account].owner == input.creditor
+debtor_is_valid if data.account.account_attributes[input.debtor_account].owner == input.debtor
+
+period_is_valid if input.period <= 30
+amount_is_valid if data.account.account_attributes[input.debtor_account].amount >= input.amount
diff --git a/test/policies/action/policy.rego b/test/policies/action/policy.rego
new file mode 100644 (file)
index 0000000..300fe50
--- /dev/null
@@ -0,0 +1,21 @@
+package action
+
+import rego.v1
+
+# By default, deny requests.
+default allow := false
+
+
+# Allow the action if admin role is granted permission to perform the action.
+allow if {
+    some i
+    data.action.user_roles[input.user][i] == role
+    some j
+    data.action.role_permissions[role].actions[j] == input.action
+    some k
+    data.action.role_permissions[role].resources[k] == input.type
+}
+#       * Rego comparison to other systems: https://www.openpolicyagent.org/docs/latest/comparison-to-other-systems/
+#       * Rego Iteration: https://www.openpolicyagent.org/docs/latest/#iteration
+
+
diff --git a/test/policies/data/abac/data.json b/test/policies/data/abac/data.json
new file mode 100644 (file)
index 0000000..77b5668
--- /dev/null
@@ -0,0 +1,94 @@
+{
+    "sensor_data": [
+        {
+            "id": "0001",
+            "location": "Sri Lanka",
+            "temperature": "28 C",
+            "precipitation": "1000 mm",
+            "windspeed": "5.5 m/s",
+            "humidity": "40%",
+            "particle_density": "1.3 g/l",
+            "timestamp": "2024-02-26"
+        },
+        {
+            "id": "0002",
+            "location": "Colombo",
+            "temperature": "30 C",
+            "precipitation": "1200 mm",
+            "windspeed": "6.0 m/s",
+            "humidity": "45%",
+            "particle_density": "1.5 g/l",
+            "timestamp": "2024-02-26"
+        },
+        {
+            "id": "0003",
+            "location": "Kandy",
+            "temperature": "25 C",
+            "precipitation": "800 mm",
+            "windspeed": "4.5 m/s",
+            "humidity": "60%",
+            "particle_density": "1.1 g/l",
+            "timestamp": "2024-02-26"
+        },
+        {
+            "id": "0004",
+            "location": "Galle",
+            "temperature": "35 C",
+            "precipitation": "500 mm",
+            "windspeed": "7.2 m/s",
+            "humidity": "30%",
+            "particle_density": "1.8 g/l",
+            "timestamp": "2024-02-27"
+        },
+        {
+            "id": "0005",
+            "location": "Jaffna",
+            "temperature": "-5 C",
+            "precipitation": "300 mm",
+            "windspeed": "3.8 m/s",
+            "humidity": "20%",
+            "particle_density": "0.9 g/l",
+            "timestamp": "2024-02-27"
+        },
+        {
+            "id": "0006",
+            "location": "Trincomalee",
+            "temperature": "20 C",
+            "precipitation": "1000 mm",
+            "windspeed": "5.0 m/s",
+            "humidity": "55%",
+            "particle_density": "1.2 g/l",
+            "timestamp": "2024-02-28"
+        },
+        {
+            "id": "0007",
+            "location": "Nuwara Eliya",
+            "temperature": "25 C",
+            "precipitation": "600 mm",
+            "windspeed": "4.0 m/s",
+            "humidity": "50%",
+            "particle_density": "1.3 g/l",
+            "timestamp": "2024-02-28"
+        },
+        {
+            "id": "0008",
+            "location": "Anuradhapura",
+            "temperature": "28 C",
+            "precipitation": "700 mm",
+            "windspeed": "5.8 m/s",
+            "humidity": "40%",
+            "particle_density": "1.4 g/l",
+            "timestamp": "2024-02-29"
+        },
+        {
+            "id": "0009",
+            "location": "Matara",
+            "temperature": "32 C",
+            "precipitation": "900 mm",
+            "windspeed": "6.5 m/s",
+            "humidity": "65%",
+            "particle_density": "1.6 g/l",
+            "timestamp": "2024-02-29"
+        }
+    ]
+}
diff --git a/test/policies/data/account/data.json b/test/policies/data/account/data.json
new file mode 100644 (file)
index 0000000..df263d3
--- /dev/null
@@ -0,0 +1,16 @@
+{
+  "account_attributes":{
+    "11111":{
+      "owner":"alice",
+      "amount":10000
+    },
+    "22222":{
+      "owner":"bob",
+      "amount":10000
+    },
+    "33333":{
+      "owner":"cam",
+      "amount":10000
+    }
+  }
+}
diff --git a/test/policies/data/action/data.json b/test/policies/data/action/data.json
new file mode 100644 (file)
index 0000000..99145b7
--- /dev/null
@@ -0,0 +1,43 @@
+{
+    "user_roles": {
+        "alice": [
+            "admin"
+        ],
+        "bob": [
+            "editor"
+        ],
+        "charlie": [
+            "viewer"
+        ]
+    },
+    "role_permissions": {
+        "admin": {
+            "actions": [
+                "read",
+                "write",
+                "delete"
+            ],
+            "resources": [
+                "server",
+                "database"
+            ]
+        },
+        "editor": {
+            "actions": [
+                "read",
+                "write"
+            ],
+            "resources": [
+                "server"
+            ]
+        },
+        "viewer": {
+            "actions": [
+                "read"
+            ],
+            "resources": [
+                "server"
+            ]
+        }
+    }
+}
diff --git a/test/policies/data/organization/data.json b/test/policies/data/organization/data.json
new file mode 100644 (file)
index 0000000..35fe4a1
--- /dev/null
@@ -0,0 +1,32 @@
+{
+    "acls": [
+        {
+            "user": "alice",
+            "actions": [
+                "edit",
+                "read"
+            ],
+            "component": "component_A",
+            "project": "project_A",
+            "organization": "org_A"
+        },
+        {
+            "user": "bob",
+            "actions": ["read"],
+            "organization": "org_A"
+        },
+        {
+            "user": "bob",
+            "action": ["edit"],
+            "component": "component_A",
+            "project": "project_B",
+            "organization": "org_A"
+        },
+        {
+            "user": "charlie",
+            "action": ["read"],
+            "project": "project_B",
+            "organization": "org_A"
+        }
+    ]
+}
diff --git a/test/policies/data/role/data.json b/test/policies/data/role/data.json
new file mode 100644 (file)
index 0000000..88ac41b
--- /dev/null
@@ -0,0 +1,63 @@
+{
+    "user_roles": {
+        "alice": [
+            "admin"
+        ],
+        "bob": [
+            "employee",
+            "billing"
+        ],
+        "eve": [
+            "customer"
+        ]
+    },
+    "role_grants": {
+        "customer": [
+            {
+                "action": "read",
+                "type": "dog"
+            },
+            {
+                "action": "read",
+                "type": "cat"
+            },
+            {
+                "action": "adopt",
+                "type": "dog"
+            },
+            {
+                "action": "adopt",
+                "type": "cat"
+            }
+        ],
+        "employee": [
+            {
+                "action": "read",
+                "type": "dog"
+            },
+            {
+                "action": "read",
+                "type": "cat"
+            },
+            {
+                "action": "update",
+                "type": "dog"
+            },
+            {
+                "action": "update",
+                "type": "cat"
+            }
+        ],
+        "billing": [
+            {
+                "action": "read",
+                "type": "finance"
+            },
+            {
+                "action": "update",
+                "type": "finance"
+            }
+        ]
+    }
+}
+
diff --git a/test/policies/example/policy.rego b/test/policies/example/policy.rego
new file mode 100644 (file)
index 0000000..cc19285
--- /dev/null
@@ -0,0 +1,13 @@
+package example
+
+import rego.v1
+
+allow if {
+        input.path == ["users"]
+        input.method == "POST"
+}
+
+allow if {
+        input.path == ["users", input.user_id]
+        input.method == "GET"
+}
diff --git a/test/policies/organization/policy.rego b/test/policies/organization/policy.rego
new file mode 100644 (file)
index 0000000..31e7fb6
--- /dev/null
@@ -0,0 +1,38 @@
+package organization
+
+import rego.v1
+
+default allow := false
+
+# organization level access
+allow if {
+ some acl in data.organization.acls
+ acl.user == input.user
+ acl.organization == input.organization
+ acl.project == input.project
+ acl.component == input.component
+
+ some action in acl.actions
+ action == input.action
+}
+
+# project level access
+allow if {
+ some acl in data.organization.acls
+ acl.user == input.user
+ acl.organization == input.organization
+ acl.project == input.project
+
+ some action in acl.actions
+ action == input.action
+}
+
+# component level access
+allow if {
+ some acl in data.organization.acls
+ acl.user == input.user
+ acl.organization == input.organization
+
+ some action in acl.actions
+ action == input.action
+}
diff --git a/test/policies/role/policy.rego b/test/policies/role/policy.rego
new file mode 100644 (file)
index 0000000..54bdecf
--- /dev/null
@@ -0,0 +1,53 @@
+# Role-based Access Control (RBAC)
+# --------------------------------
+#
+# This example defines an RBAC model for a Pet Store API. The Pet Store API allows
+# users to look at pets, adopt them, update their stats, and so on. The policy
+# controls which users can perform actions on which resources. The policy implements
+# a classic Role-based Access Control model where users are assigned to roles and
+# roles are granted the ability to perform some action(s) on some type of resource.
+#
+# This example shows how to:
+#
+#      * Define an RBAC model in Rego that interprets role mappings represented in JSON.
+#      * Iterate/search across JSON data structures (e.g., role mappings)
+#
+# For more information see:
+#package app.rbac
+package role
+
+import rego.v1
+
+# By default, deny requests.
+default allow := false
+
+# Allow admins to do anything.
+allow if user_is_admin
+
+# Allow the action if the user is granted permission to perform the action.
+allow if {
+       # Find grants for the user.
+       some grant in user_is_granted
+
+       # Check if the grant permits the action.
+       input.action == grant.action
+       input.type == grant.type
+}
+
+# user_is_admin is true if "admin" is among the user's roles as per data.user_roles
+user_is_admin if "admin" in data.role.user_roles[input.user]
+
+# user_is_granted is a set of grants for the user identified in the request.
+# The `grant` will be contained if the set `user_is_granted` for every...
+user_is_granted contains grant if {
+       # `role` assigned an element of the user_roles for this user...
+       some role in data.role.user_roles[input.user]
+
+       # `grant` assigned a single grant from the grants list for 'role'...
+       some grant in data.role.role_grants[role]
+}
+
+#      * Rego comparison to other systems: https://www.openpolicyagent.org/docs/latest/comparison-to-other-systems/
+#      * Rego Iteration: https://www.openpolicyagent.org/docs/latest/#iteration
+
+
diff --git a/test/policy-new.yaml b/test/policy-new.yaml
new file mode 100644 (file)
index 0000000..2fbcf79
--- /dev/null
@@ -0,0 +1,21 @@
+tosca_definitions_version: tosca_simple_yaml_1_1_0
+policy_types:
+  onap.policies.Native:
+    derived_from: tosca.policies.Root
+    description: a base policy type for all native PDP policies
+    version: 1.0.0
+    name: onap.policies.Native
+  onap.policies.native.opa:
+    derived_from: onap.policies.Native
+    version: 1.0.0
+    name: onap.policies.native.opa
+    description: a policy type for native opa policies
+    properties:
+      policy:
+        type: string
+        type_version: 0.0.0
+        description: The rego PolicySet or Policy
+        required: true
+        metadata:
+          encoding: Base64
+
diff --git a/test/scripts.sh b/test/scripts.sh
new file mode 100755 (executable)
index 0000000..ab4f838
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/bash
+# Set up credentials and host variables
+USER="policyadmin"
+PASSWORD="zb!XztG34"
+HOST="localhost"
+# Exit immediately if a command exits with a non-zero status
+set -e
+# Step 1: Create a Policy
+echo "Creating a new policy..."
+sleep 40
+curl -u "$USER:$PASSWORD" --header "Content-Type: application/yaml" \
+     -X POST --data-binary @policy-new.yaml \
+     http://policy-api:6969/policy/api/v1/policytypes
+echo "Policy created successfully. Check policy-api logs for details."
+# Step 2: Create Groups
+echo "Creating groups..."
+curl -u "$USER:$PASSWORD" --header "Content-Type: application/json" \
+     -X POST --data-binary @Opagroup.json \
+     http://policy-pap:6969/policy/pap/v1/pdps/groups/batch
+echo "Groups created successfully. Check policy-pap logs for details."
+echo "Script execution completed."
diff --git a/test/scripts.txt b/test/scripts.txt
new file mode 100644 (file)
index 0000000..3d60d4a
--- /dev/null
@@ -0,0 +1,21 @@
+ curl -u 'policyadmin:zb!XztG34' --header "Content-Type:application/yaml"  -X POST --data-binary @policy-new.yaml http://localhost:30002/policy/api/v1/policytypes
+
+# policy-new.yaml is inside test directory to create policy 
+#check policy-api logs
+
+
+//Create Groups
+
+curl -u 'policyadmin:zb!XztG34' --header "Content-Type:application/json"  -X POST --data-binary  @Opagroup.json http://localhost:30003/policy/pap/v1/pdps/groups/batch
+
+#Check policy-pap logs
+#file Opagroup.json is inside test
+
+// Sends registration message to policy-pdp-pap
+
+docker exec -it kafka /bin/sh 
+
+echo '{"messageName": "PDP_STATUS", "requestId": "e9b4ee77-5400-41a8-87ba-3c914a86ee08", "timestampMs": "1728551661460","name": "opa-2e953ecf-40f1-47f7-8a5e-53031947516c","pdpGroup": "opaGroup","pdpSubgroup": null, "pdpType": "opa","state": "PASSIVE","healthy": "HEALTHY", "description": null, "policies": []}' |  kafka-console-producer --broker-list kafka:9092 --topic policy-pdp-pap
+
+
+#To get Gracefulshutdown signals commented command and changed entrypoint to /app/opa-pdp
diff --git a/test/wait_for_port.sh b/test/wait_for_port.sh
new file mode 100644 (file)
index 0000000..b29102b
--- /dev/null
@@ -0,0 +1,91 @@
+#!/bin/sh
+# ============LICENSE_START====================================================
+#  Copyright (C) 2021 AT&T Intellectual Property. All rights reserved.
+#  Modifications Copyright (C) 2022-2023 Nordix Foundation.
+# =============================================================================
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# ============LICENSE_END======================================================
+
+usage() {
+    echo args: [-t timeout] [-c command] hostname1 port1 hostname2 port2 ... >&2
+    exit 1
+}
+
+tmout=300
+cmd=
+while getopts c:t: opt
+do
+    case "$opt" in
+        c)
+            cmd="$OPTARG"
+            ;;
+
+        t)
+            tmout="$OPTARG"
+            ;;
+
+        *)
+            usage
+            ;;
+    esac
+done
+
+nargs=$((OPTIND-1))
+shift "$nargs"
+
+even_args=$(($#%2))
+if [ $# -lt 2 ] || [ "$even_args" -ne 0 ]
+then
+    usage
+fi
+
+while [ $# -ge 2 ]
+do
+    export host="$1"
+    export port="$2"
+    shift
+    shift
+
+    echo "Waiting for $host port $port..."
+
+    while [ "$tmout" -gt 0 ]
+    do
+        if command -v docker > /dev/null 2>&1
+        then
+            docker ps --format "table {{ .Names }}\t{{ .Status }}"
+        fi
+
+        nc -vz "$host" "$port"
+        rc=$?
+
+        if [ $rc -eq 0 ]
+        then
+            break
+        else
+            tmout=$((tmout-1))
+            sleep 1
+        fi
+    done
+
+    if [ $rc -ne 0 ]
+    then
+        echo "$host port $port cannot be reached"
+        exit $rc
+    fi
+done
+#sh scripts.sh
+$cmd
+
+exit 0
diff --git a/version b/version
new file mode 100644 (file)
index 0000000..3eefcb9
--- /dev/null
+++ b/version
@@ -0,0 +1 @@
+1.0.0