diff options
90 files changed, 7872 insertions, 1 deletions
diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..389c328 --- /dev/null +++ b/Dockerfile @@ -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 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 @@ -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 index 0000000..aaff2b9 --- /dev/null +++ b/api/openapi.yaml @@ -0,0 +1,374 @@ +#
+# ========================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===================================
+#
+openapi: 3.0.3
+info:
+ title: "Policy OPA PDP Documentation"
+ description: Policy OPA PDP Service
+ version: 1.0.0
+ x-component: Policy Framework
+ x-planned-retirement-date: tbd
+ contact:
+ name: Deena Mukundan
+ email: dm00536893@techmahindra.com
+servers:
+- url: http://policy-opa-pdp:8282/policy/pdpx/v1
+- url: https://policy-opa-pdp:8282/policy/pdpx/v1
+tags:
+- name: Decision
+- name: Statistics
+- name: HealthCheck
+paths:
+ /decision:
+ post:
+ tags:
+ - Decision
+ summary: Fetch the decision using specified decision parameters
+ description: Returns the policy decision from Policy OPA PDP
+ operationId: decision
+ parameters:
+ - name: X-ONAP-RequestID
+ in: header
+ description: RequestID for http transaction
+ schema:
+ type: string
+ format: uuid
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OPADecisionRequest'
+ application/yaml:
+ schema:
+ $ref: '#/components/schemas/OPADecisionRequest'
+ required: false
+ responses:
+ 200:
+ description: successful operation
+ headers:
+ X-LatestVersion:
+ description: Used only to communicate an API's latest version
+ schema:
+ type: string
+ X-PatchVersion:
+ description: Used only to communicate a PATCH version in a response
+ for troubleshooting purposes only, and will not be provided by the
+ client on request
+ schema:
+ type: string
+ X-MinorVersion:
+ description: Used to request or communicate a MINOR version back from
+ the client to the server, and from the server back to the client
+ schema:
+ type: string
+ X-ONAP-RequestID:
+ description: Used to track REST transactions for logging purpose
+ schema:
+ type: string
+ format: uuid
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OPADecisionResponse'
+ application/yaml:
+ schema:
+ $ref: '#/components/schemas/OPADecisionResponse'
+ 400:
+ description: Bad Request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ application/yaml:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ 401:
+ description: Authentication Error
+ content: {}
+ 403:
+ description: Authorization Error
+ content: {}
+ 500:
+ description: Internal Server Error
+ content: {}
+ security:
+ - basicAuth: []
+ x-interface info:
+ last-mod-release: Paris
+ pdpx-version: 1.0.0
+ x-codegen-request-body-name: body
+ /healthcheck:
+ get:
+ tags:
+ - HealthCheck
+ summary: Perform a system healthcheck
+ description: Provides healthy status of the Policy OPA PDP component
+ operationId: healthcheck
+ parameters:
+ - name: X-ONAP-RequestID
+ in: header
+ description: RequestID for http transaction
+ schema:
+ type: string
+ format: uuid
+ responses:
+ 200:
+ description: successful operation
+ headers:
+ X-LatestVersion:
+ description: Used only to communicate an API's latest version
+ schema:
+ type: string
+ X-PatchVersion:
+ description: Used only to communicate a PATCH version in a response
+ for troubleshooting purposes only, and will not be provided by the
+ client on request
+ schema:
+ type: string
+ X-MinorVersion:
+ description: Used to request or communicate a MINOR version back from
+ the client to the server, and from the server back to the client
+ schema:
+ type: string
+ X-ONAP-RequestID:
+ description: Used to track REST transactions for logging purpose
+ schema:
+ type: string
+ format: uuid
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HealthCheckReport'
+ application/yaml:
+ schema:
+ $ref: '#/components/schemas/HealthCheckReport'
+ 401:
+ description: Authentication Error
+ content: {}
+ 403:
+ description: Authorization Error
+ content: {}
+ 500:
+ description: Internal Server Error
+ content: {}
+ security:
+ - basicAuth: []
+ x-interface info:
+ last-mod-release: Paris
+ pdpx-version: 1.0.0
+ /statistics:
+ get:
+ tags:
+ - Statistics
+ summary: Fetch current statistics
+ description: Provides current statistics of the Policy OPA PDP component
+ operationId: statistics
+ parameters:
+ - name: X-ONAP-RequestID
+ in: header
+ description: RequestID for http transaction
+ schema:
+ type: string
+ format: uuid
+ responses:
+ 200:
+ description: successful operation
+ headers:
+ X-LatestVersion:
+ description: Used only to communicate an API's latest version
+ schema:
+ type: string
+ X-PatchVersion:
+ description: Used only to communicate a PATCH version in a response
+ for troubleshooting purposes only, and will not be provided by the
+ client on request
+ schema:
+ type: string
+ X-MinorVersion:
+ description: Used to request or communicate a MINOR version back from
+ the client to the server, and from the server back to the client
+ schema:
+ type: string
+ X-ONAP-RequestID:
+ description: Used to track REST transactions for logging purpose
+ schema:
+ type: string
+ format: uuid
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/StatisticsReport'
+ application/yaml:
+ schema:
+ $ref: '#/components/schemas/StatisticsReport'
+ 401:
+ description: Authentication Error
+ content: {}
+ 403:
+ description: Authorization Error
+ content: {}
+ 500:
+ description: Internal Server Error
+ content: {}
+ security:
+ - basicAuth: []
+ x-interface info:
+ last-mod-release: Paris
+ pdpx-version: 1.0.0
+components:
+ schemas:
+ ErrorResponse:
+ type: object
+ properties:
+ responseCode:
+ type: string
+ enum:
+ - BAD_REQUEST
+ - UNAUTHORIZED
+ - METHOD_NOT_ALLOWED
+ - NOT_ACCEPTABLE
+ - REQUEST_TIMEOUT
+ - CONFLICT
+ - GONE
+ - LENGTH_REQUIRED
+ - PRECONDITION_FAILED
+ - REQUEST_ENTITY_TOO_LARGE
+ - REQUEST_URI_TOO_LONG
+ - UNSUPPORTED_MEDIA_TYPE
+ - REQUESTED_RANGE_NOT_SATISFIABLE
+ - EXPECTATION_FAILED
+ - PRECONDITION_REQUIRED
+ - TOO_MANY_REQUESTS
+ - REQUEST_HEADER_FIELDS_TOO_LARGE
+ - INTERNAL_SERVER_ERROR
+ - NOT_IMPLEMENTED
+ - BAD_GATEWAY
+ - SERVICE_UNAVAILABLE
+ - GATEWAY_TIMEOUT
+ - HTTP_VERSION_NOT_SUPPORTED
+ - NETWORK_AUTHENTICATION_REQUIRED
+ errorMessage:
+ type: string
+ policyName:
+ type: string
+ errorDetails:
+ type: array
+ items:
+ type: string
+ OPADecisionRequest:
+ type: object
+ properties:
+ onapName:
+ type: string
+ onapComponent:
+ type: string
+ onapInstance:
+ type: string
+ currentDateTime:
+ type: string
+ format: date-time
+ currentDate:
+ type: string
+ format: date
+ currentTime:
+ type: string
+ format: date-time
+ timeZone:
+ type: string
+ description: "Timezone in IANA format (e.g., 'America/NewYork', 'Europe/Paris', 'UTC')"
+ timeOffset:
+ type: string
+ pattern: '^[+-]?\d{2}:\d{2}$'
+ description: "Time offset in hours and minutes, e.g., '+02:00' or '-05:00'"
+ policyName:
+ type: string
+ input:
+ type: object
+ additionalProperties: true
+ example:
+ user: alice
+ action: read
+ object: id123
+ type: dog
+ HealthCheckReport:
+ type: object
+ properties:
+ name:
+ type: string
+ url:
+ type: string
+ healthy:
+ type: boolean
+ code:
+ type: integer
+ format: int32
+ message:
+ type: string
+ OPADecisionResponse:
+ type: object
+ properties:
+ statusMessage:
+ type: string
+ decision:
+ type: string
+ enum:
+ - PERMIT
+ - DENY
+ - INDETERMINATE
+ policyName:
+ type: string
+ StatisticsReport:
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int32
+ totalPolicyTypesCount:
+ type: integer
+ format: int64
+ totalPoliciesCount:
+ type: integer
+ format: int64
+ totalErrorCount:
+ type: integer
+ format: int64
+ permitDecisionsCount:
+ type: integer
+ format: int64
+ denyDecisionsCount:
+ type: integer
+ format: int64
+ deploySuccessCount:
+ type: integer
+ format: int64
+ deployFailureCount:
+ type: integer
+ format: int64
+ undeploySuccessCount:
+ type: integer
+ format: int64
+ undeployFailureCount:
+ type: integer
+ format: int64
+ indeterminantDecisionsCount:
+ type: integer
+ format: int64
+ securitySchemes:
+ basicAuth:
+ type: http
+ description: ""
+ scheme: basic
\ No newline at end of file diff --git a/api/register-handlers.go b/api/register-handlers.go new file mode 100644 index 0000000..37028d2 --- /dev/null +++ b/api/register-handlers.go @@ -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 index 0000000..72624f8 --- /dev/null +++ b/api/register-handlers_test.go @@ -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 index 0000000..2905d77 --- /dev/null +++ b/build/Dockerfile @@ -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 index 0000000..d459215 --- /dev/null +++ b/build/Makefile @@ -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 index 0000000..9b44a47 --- /dev/null +++ b/build/build_image.sh @@ -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 index 0000000..4840688 --- /dev/null +++ b/cfg/config.go @@ -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 index 0000000..fe91804 --- /dev/null +++ b/cfg/config_test.go @@ -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 index 0000000..0def78f --- /dev/null +++ b/cmd/opa-pdp/opa-pdp.go @@ -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 index 0000000..9da4c41 --- /dev/null +++ b/cmd/opa-pdp/opa-pdp_test.go @@ -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 index 0000000..601608f --- /dev/null +++ b/consts/constants.go @@ -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" +) @@ -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 +) @@ -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 index 0000000..fe48de0 --- /dev/null +++ b/pkg/bundleserver/bundle-server.go @@ -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 index 0000000..eda22b5 --- /dev/null +++ b/pkg/bundleserver/bundle-server_test.go @@ -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 index 0000000..374aabf --- /dev/null +++ b/pkg/decision/decision-provider.go @@ -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 index 0000000..c8a1bf6 --- /dev/null +++ b/pkg/decision/decision-provider_test.go @@ -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 index 0000000..4c8a13b --- /dev/null +++ b/pkg/healthcheck/healthcheck.go @@ -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 index 0000000..3e1876f --- /dev/null +++ b/pkg/healthcheck/healthcheck_test.go @@ -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 index 0000000..8d7da92 --- /dev/null +++ b/pkg/kafkacomm/handler/pdp_message_handler.go @@ -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 index 0000000..3764c9e --- /dev/null +++ b/pkg/kafkacomm/handler/pdp_message_handler_test.go @@ -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 index 0000000..32d998f --- /dev/null +++ b/pkg/kafkacomm/handler/pdp_state_change_handler.go @@ -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 index 0000000..f7e8f84 --- /dev/null +++ b/pkg/kafkacomm/handler/pdp_state_change_handler_test.go @@ -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 index 0000000..632bcc8 --- /dev/null +++ b/pkg/kafkacomm/handler/pdp_update_message_handler.go @@ -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 index 0000000..061f1ce --- /dev/null +++ b/pkg/kafkacomm/handler/pdp_update_message_handler_test.go @@ -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 index 0000000..ca5140e --- /dev/null +++ b/pkg/kafkacomm/mocks/kafkaconsumerinterface.go @@ -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 index 0000000..97b6f53 --- /dev/null +++ b/pkg/kafkacomm/mocks/kafkaproducerinterface.go @@ -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 index 0000000..4858bdf --- /dev/null +++ b/pkg/kafkacomm/pdp_topic_consumer.go @@ -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 index 0000000..2fdfa90 --- /dev/null +++ b/pkg/kafkacomm/pdp_topic_consumer_test.go @@ -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 index 0000000..1b11b35 --- /dev/null +++ b/pkg/kafkacomm/pdp_topic_producer.go @@ -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 index 0000000..55f3bc8 --- /dev/null +++ b/pkg/kafkacomm/pdp_topic_producer_test.go @@ -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 index 0000000..f9cc279 --- /dev/null +++ b/pkg/kafkacomm/publisher/mocks/PdpStatusSender.go @@ -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 index 0000000..f814992 --- /dev/null +++ b/pkg/kafkacomm/publisher/pdp-heartbeat.go @@ -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 index 0000000..f03b0eb --- /dev/null +++ b/pkg/kafkacomm/publisher/pdp-heartbeat_test.go @@ -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 index 0000000..75f22d6 --- /dev/null +++ b/pkg/kafkacomm/publisher/pdp-pap-registration.go @@ -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 index 0000000..03749de --- /dev/null +++ b/pkg/kafkacomm/publisher/pdp-pap-registration_test.go @@ -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 index 0000000..756d0f2 --- /dev/null +++ b/pkg/kafkacomm/publisher/pdp-status-publisher.go @@ -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 index 0000000..5e02704 --- /dev/null +++ b/pkg/kafkacomm/publisher/pdp-status-publisher_test.go @@ -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 index 0000000..2a8b997 --- /dev/null +++ b/pkg/log/log.go @@ -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 index 0000000..d24274c --- /dev/null +++ b/pkg/log/log_test.go @@ -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 index 0000000..2fc9539 --- /dev/null +++ b/pkg/metrics/counters.go @@ -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 index 0000000..ef4c2b0 --- /dev/null +++ b/pkg/metrics/counters_test.go @@ -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 index 0000000..67cee79 --- /dev/null +++ b/pkg/metrics/statistics-provider.go @@ -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 index 0000000..a5e57b6 --- /dev/null +++ b/pkg/metrics/statistics-provider_test.go @@ -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 index 0000000..8b0d9db --- /dev/null +++ b/pkg/model/healthcheckmessage.go @@ -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 index 0000000..a4451d7 --- /dev/null +++ b/pkg/model/mesages.go @@ -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 index 0000000..f6bb5ca --- /dev/null +++ b/pkg/model/messages_test.go @@ -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 index 0000000..4f1b770 --- /dev/null +++ b/pkg/model/oapicodegen/models.go @@ -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 index 0000000..387a1e8 --- /dev/null +++ b/pkg/model/pdphealthstatus.go @@ -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 index 0000000..0cb89cf --- /dev/null +++ b/pkg/model/pdphealthstatus_test.go @@ -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 index 0000000..8febae5 --- /dev/null +++ b/pkg/model/pdpresponsedetails.go @@ -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 index 0000000..14b9cd8 --- /dev/null +++ b/pkg/model/pdpresponsedetails_test.go @@ -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 index 0000000..2b54d16 --- /dev/null +++ b/pkg/model/pdpstate.go @@ -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 index 0000000..35ff6af --- /dev/null +++ b/pkg/model/pdpstate_test.go @@ -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 index 0000000..7afc7b1 --- /dev/null +++ b/pkg/model/toscaconceptidentifier.go @@ -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 index 0000000..a131483 --- /dev/null +++ b/pkg/model/toscaconceptidentifier_test.go @@ -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 index 0000000..da6c7cc --- /dev/null +++ b/pkg/opasdk/opasdk.go @@ -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 index 0000000..b6c205b --- /dev/null +++ b/pkg/opasdk/opasdk_test.go @@ -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 index 0000000..70744fd --- /dev/null +++ b/pkg/pdpattributes/pdpattributes.go @@ -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 index 0000000..0870ed6 --- /dev/null +++ b/pkg/pdpattributes/pdpattributes_test.go @@ -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 index 0000000..0adaa2e --- /dev/null +++ b/pkg/pdpstate/pdpstate.go @@ -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 index 0000000..6b7078c --- /dev/null +++ b/pkg/pdpstate/pdpstate_test.go @@ -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 index 0000000..c2cb591 --- /dev/null +++ b/pkg/utils/utils.go @@ -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 index 0000000..b70fa2b --- /dev/null +++ b/pkg/utils/utils_test.go @@ -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 index 0000000..002b962 --- /dev/null +++ b/test/Opagroup.json @@ -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 index 0000000..7940342 --- /dev/null +++ b/test/README.md @@ -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 index 0000000..3f2aa43 --- /dev/null +++ b/test/config.json @@ -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 index 0000000..3f2aa43 --- /dev/null +++ b/test/config/opa-pdp/config.json @@ -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 index 0000000..502700c --- /dev/null +++ b/test/config/opa-pdp/groups.json @@ -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 index 0000000..7ed14cb --- /dev/null +++ b/test/config/opa-pdp/policy-opa-pdp.sh @@ -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 index 0000000..6778882 --- /dev/null +++ b/test/docker-compose.yml @@ -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 index 0000000..9dc6ea9 --- /dev/null +++ b/test/policies/abac/policy.rego @@ -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 index 0000000..f99e8eb --- /dev/null +++ b/test/policies/account/policy.rego @@ -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 index 0000000..300fe50 --- /dev/null +++ b/test/policies/action/policy.rego @@ -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 index 0000000..77b5668 --- /dev/null +++ b/test/policies/data/abac/data.json @@ -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 index 0000000..df263d3 --- /dev/null +++ b/test/policies/data/account/data.json @@ -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 index 0000000..99145b7 --- /dev/null +++ b/test/policies/data/action/data.json @@ -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 index 0000000..35fe4a1 --- /dev/null +++ b/test/policies/data/organization/data.json @@ -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 index 0000000..88ac41b --- /dev/null +++ b/test/policies/data/role/data.json @@ -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 index 0000000..cc19285 --- /dev/null +++ b/test/policies/example/policy.rego @@ -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 index 0000000..31e7fb6 --- /dev/null +++ b/test/policies/organization/policy.rego @@ -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 index 0000000..54bdecf --- /dev/null +++ b/test/policies/role/policy.rego @@ -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 index 0000000..2fbcf79 --- /dev/null +++ b/test/policy-new.yaml @@ -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 index 0000000..ab4f838 --- /dev/null +++ b/test/scripts.sh @@ -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 index 0000000..3d60d4a --- /dev/null +++ b/test/scripts.txt @@ -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 index 0000000..b29102b --- /dev/null +++ b/test/wait_for_port.sh @@ -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 @@ -0,0 +1 @@ +1.0.0 |