diff --git a/README.md b/README.md index 6999855..f2a9605 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ **Note:** If you need help accessing the EIAP Ecosystem, contact support -at this **email address:** intelligent.automation.platform@ericsson.com +at this **email address:** ## Introduction @@ -24,7 +24,14 @@ This is a simple Hello World Python App with the following three endpoints: the number of successful and failed invocations of the '/sample-app/python/hello' endpoint. -## Build Docker Image +## Build Docker + +Extract the downloaded sample App package. Using a command line tool, + go inside the extracted `eric-oss-hello-world-python-app-` directory. + +```bash +cd /eric-oss-hello-world-python-app- +``` Rename the `Dockerfile-template` file to `Dockerfile`. @@ -79,6 +86,9 @@ with the correct Python Sample App version. Run the following commands from within your project directory `eric-oss-hello-world-python-app-`. +**Note:** X.509 certificates are used for authentication, and + mTLS uses them to secure communication between the App and the platform. + ```bash mkdir -p helloworldAppPackage ``` @@ -102,7 +112,7 @@ Move the created .tgz file to the OtherDefinitions/ASD directory. mv eric-oss-hello-world-python-app-.tgz ./helloworldAppPackage/OtherDefinitions/ASD/ ``` -Create a folder where the CSAR app package will be stored into. +Create a folder where the CSAR App package will be stored into. ```bash mkdir csar-output @@ -115,7 +125,7 @@ directory. docker save proj-eric-oss-drop/eric-oss-hello-world-python-app: -o csar-output/docker.tar ``` -Run the following command locally to create a CSAR app package using the +Run the following command locally to create a CSAR App package using the eric-oss-app-package-tool. ```bash @@ -147,11 +157,11 @@ ls ./csar-output Contact the platform administrator to request the following: -- A CA certificate, needed for secure communication with the platform APIs. -- Client Access to the platform with the required roles, needed for authorized +- A Client certificate, Client key and platform CA certificate, + needed for secure communication with the platform APIs. +- Client ID to access the platform with the required roles, needed for authorized communication with the platform APIs used to *onboard and instantiate* the -Hello World App. You will receive a Client ID and Client Secret from the -platform administrator. Include the required roles listed below in the request. +Hello World App. Include the required roles listed below in the request. | Role | Role Description | | --------------------------------------------------------------- | ----------------------------------------------------------------------------- | @@ -163,13 +173,15 @@ platform administrator. Include the required roles listed below in the request. See [Client Access to REST APIs](https://developer.intelligentautomationplatform.ericsson.net/#tutorials/client-access) for further details on client authentication. +**Note:** Replace `` with the valid `R1 host` provided by platform administrator. + Use the following command to generate a valid access token: ```bash -curl --cacert --request POST \ +curl --cert --key --cacert --request POST \ https:///auth/realms/master/protocol/openid-connect/token \ --header 'content-type: application/x-www-form-urlencoded' \ ---data "grant_type=client_credentials&client_id=&client_secret=" +--data "grant_type=client_credentials&client_id=" ``` This command returns an access token, which is used in the commands in the @@ -189,11 +201,10 @@ access token: Onboard the **Hello World CSAR App Package** using [App Administration](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-administration/developer-guide-manage?chapter=onboard). -To start the onboarding of the Hello World CSAR app, -run the following command in a command line tool. +Run the following command. ```bash -curl --cacert --location --request POST 'https:///app-onboarding/v2/app-packages' \ +curl --cert --key --cacert --location --request POST 'https:///app-onboarding/v2/app-packages' \ --header 'Authorization: Bearer ' \ --header 'accept: application/json' \ --form 'file=@"/helloworldAppPackage.csar"' @@ -205,19 +216,19 @@ Example of command result: { "fileName": "helloworldAppPackage.csar", "onboardingJob": { - "id": "a2f0a43d-730a-4991-8481-746c3e76556e", - "href": "app-onboarding/v2/onboarding-jobs/a2f0a43d-730a-4991-8481-746c3e76556e" + "id": "af036040-a732-4af9-b65a-8103da56c35c", + "href": "/onboarding-jobs/af036040-a732-4af9-b65a-8103da56c35c" } } ``` An onboarding-job `id` is shown in the command result -(a2f0a43d-730a-4991-8481-746c3e76556e in the example). +(af036040-a732-4af9-b65a-8103da56c35c in the example). This is the `JOB_ID`. Use the `JOB_ID` to get the status of the onboarding process in the following commands: ```bash -curl --cacert --location --request GET 'https:///app-onboarding/v2/onboarding-jobs/' \ +curl --cert --key --cacert --location --request GET 'https:///app-onboarding/v2/onboarding-jobs/' \ --header 'Authorization: Bearer ' \ --header 'accept: application/json' ``` @@ -229,37 +240,43 @@ Example of command result: ```json { - "id": "a2f0a43d-730a-4991-8481-746c3e76556e", + "id": "af036040-a732-4af9-b65a-8103da56c35c", "fileName": "helloworldAppPackage.csar", "packageVersion": "3.1.1-0", - "packageSize": "53.1282MiB", + "packageSize": "51.7659MiB", "vendor": "Ericsson", "type": "rApp", - "onboardStartedAt": "2024-09-13T09:48:53.239542Z", + "onboardStartedAt": "2025-05-31T13:51:56.616Z", "status": "ONBOARDED", - "onboardEndedAt": "2024-09-13T09:49:01.299826Z", + "onboardEndedAt": "2025-05-31T13:51:59.955Z", "events": [ { "type": "INFO", - "title": "Stored 1 out of 3 artifacts", - "detail": "Uploaded eric-oss-hello-world-python-app", - "occurredAt": "2024-09-13T09:48:57.556164Z" + "title": "Stored 1 out of 4 artifacts", + "detail": "Uploaded eric-oss-hello-world-python-appASD.yaml", + "occurredAt": "2025-05-31T13:51:58.042Z" }, { "type": "INFO", - "title": "Stored 2 out of 3 artifacts", - "detail": "Uploaded eric-oss-hello-world-python-appASD.yaml", - "occurredAt": "2024-09-13T09:48:57.556165Z" + "title": "Stored 2 out of 4 artifacts", + "detail": "Uploaded eric-oss-hello-world-python-app", + "occurredAt": "2025-05-31T13:51:58.043Z" }, { "type": "INFO", - "title": "Stored 3 out of 3 artifacts", + "title": "Stored 3 out of 4 artifacts", "detail": "Uploaded docker.tar", - "occurredAt": "2024-09-13T09:49:00.962182Z" + "occurredAt": "2025-05-31T13:51:59.792Z" + }, + { + "type": "INFO", + "title": "Stored 4 out of 4 artifacts", + "detail": "Uploaded security-metadata.json", + "occurredAt": "2025-05-31T13:51:59.812Z" } ], "self": { - "href": "app-onboarding/v2/onboarding-jobs/a2f0a43d-730a-4991-8481-746c3e76556e" + "href": "/onboarding-jobs/af036040-a732-4af9-b65a-8103da56c35c" }, "app": { "id": "rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0", @@ -273,7 +290,7 @@ command (rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0 in the example). Run the following command to initialize the App. ```bash -curl --cacert --location --request POST 'https:///app-lifecycle-management/v3/apps//initialization-actions' \ +curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/apps//initialization-actions' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ -d '{"action": "INITIALIZE"}' @@ -286,7 +303,7 @@ Example of command result: "app": { "status": "INITIALIZING", "id": "rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0", - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` @@ -294,7 +311,7 @@ Example of command result: Repeat the following command until the status is changed to `INITIALIZED`. ```shell -curl --cacert --location --request GET 'https:///app-lifecycle-management/v3/apps/' \ +curl --cert --key --cacert --location --request GET 'https:///app-lifecycle-management/v3/apps/' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' ``` @@ -309,11 +326,11 @@ Example of command result: "name": "eric-oss-hello-world-python-app", "version": "3.1.1-0", "mode": "DISABLED", - "status": "INITIALIZING", - "createdAt": "2024-09-13T09:49:01.273Z", + "status": "INITIALIZED", + "createdAt": "2025-05-31T13:51:59.931Z", "components": [ { - "type": "MICROSERVICE", + "type": "ASD", "name": "eric-oss-hello-world-python-app", "version": "3.1.1-0", "artifacts": [ @@ -321,12 +338,23 @@ Example of command result: "name": "docker.tar", "type": "IMAGE" }, + { + "name": "eric-oss-hello-world-python-appASD.yaml", + "type": "OPAQUE" + }, { "name": "eric-oss-hello-world-python-app", "type": "HELM" - }, + } + ] + }, + { + "type": "SECURITYMANAGEMENT", + "name": "security-mgmt", + "version": "1.0.0", + "artifacts": [ { - "name": "eric-oss-hello-world-python-appASD.yaml", + "name": "security-metadata.json", "type": "OPAQUE" } ] @@ -339,17 +367,36 @@ Example of command result: } ], "roles": [], - "events": [], + "events": [ + { + "type": "INITIALIZE", + "title": "SUCCEEDED", + "detail": "INITIALIZE has successfully completed", + "createdAt": "2025-05-31T13:55:50.421Z" + }, + { + "type": "INITIALIZE", + "title": "STARTED", + "detail": "INITIALIZE has started", + "createdAt": "2025-05-31T13:55:34.171Z" + }, + { + "type": "CREATE", + "title": "SUCCEEDED", + "detail": "CREATE has successfully completed", + "createdAt": "2025-05-31T13:51:59.945Z" + } + ], "self": { - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` -Run the following command to switch the app mode from 'DISABLED' to 'ENABLED'. +Run the following command to switch the App mode from 'DISABLED' to 'ENABLED'. ```bash -curl --cacert --location --request PUT 'https:///app-lifecycle-management/v3/apps//mode' \ +curl --cert --key --cacert --location --request PUT 'https:///app-lifecycle-management/v3/apps//mode' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ -d '{"mode": "ENABLED"}' @@ -362,7 +409,7 @@ Example of command result: "mode": "ENABLED", "app": { "id": "rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0", - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` @@ -381,19 +428,20 @@ This section describes how the App can communicate with IAM and produce logs to certificates key, certificates, and the secrets which store them. The details of the secrets, keys, certs and EIC endpoint details will be passed to App Administration through the `userDefinedHelmParameters` when - instantiating the App. The required parameters are: - - The `iamBaseUrl`, as the `/sample-app/python/hello` endpoint of this - sample App first communicates with IAM to obtain a client token (login) - before returning the "Hello World!!" string output. - - The `platformCaCertSecretName` and `platformCaCertFileName` to enable - secure TLS communication. Refer to + instantiating the App. Refer to [App Certificate Provisioning Developer Guide](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-cert-provisioning/developer-guide) to understand how certificates are loaded into the App during - instantiation for secure communication. - - The `appSecretName`, `logEndpoint`, - `appKeyFileName`, `appCertFileName` - for mTLS communication. For more information on the variable values - required, see [App Logging Developer Guide to Produce logs](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-logging/how-to-produce-logs?chapter=identify-environment-and-secret-variables-names). + instantiation for secure communication. The required parameters are: + + - The `iamBaseUrl` must point to the `R1 host`, as the `/sample-app/python/hello` + endpoint in this sample app first communicates with IAM to obtain a + client token (login) before returning the "Hello World!!" string response. + - The`appSecretName`, `appKeyFileName`, `appCertFileName`, + `platformCaCertSecretName` and `platformCaCertFileName` to enable + secure communication between the App and the platform. + - The `logEndpoint` which facilitates streaming App logs to platform, + supports only mTLS communication. For more information on the variable + values required, see [App Logging Developer Guide to Produce logs](https://developer.intelligentautomationplatform.ericsson.net/#capabilities/app-logging/how-to-produce-logs?chapter=identify-environment-and-secret-variables-names). ### Steps for Instantiation @@ -406,7 +454,7 @@ Run the following commands to start the instantiation process using the #### Create App Instance ```shell -curl --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances' \ +curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances' \ --header 'accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ @@ -419,35 +467,44 @@ Example command result: ```json { - "id": "rapp-ericsson-eric-oss-hello-world-python-app-28057851", + "id": "rapp-ericsson-eric-oss-hello-world-python-app-68129972", "appId": "rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0", "status": "UNDEPLOYED", "credentials": { - "clientId": "rapp-ericsson-eric-oss-hello-world-python-app-28057851" + "clientId": "rapp-ericsson-eric-oss-hello-world-python-app-68129972" }, "componentInstances": [ { "name": "eric-oss-hello-world-python-app", "version": "3.1.1-0", - "type": "MICROSERVICE", + "type": "ASD", "deployState": "UNDEPLOYED", "properties": { + "userDefinedHelmParameters": {}, "namespace": "", "timeout": 5 } + }, + { + "name": "security-mgmt", + "version": "1.0.0", + "type": "SECURITYMANAGEMENT", + "properties": { + "authenticatorType": "client-x509" + } } ], "self": { - "href": "/app-lifecycle-management/v3/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-28057851" + "href": "/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-68129972" }, "app": { - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` An app-instance `id` is shown in the command result - (rapp-ericsson-eric-oss-hello-world-python-app-28057851 in the example). This + (rapp-ericsson-eric-oss-hello-world-python-app-68129972 in the example). This is the `APP_INSTANCE_ID` used in the following commands. #### Deploy App Instance @@ -456,7 +513,7 @@ An app-instance `id` is shown in the command result of your App. ```shell -curl --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances//deployment-actions' \ +curl --cert --key --cacert --location --request POST 'https:///app-lifecycle-management/v3/app-instances//deployment-actions' \ --header 'accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ @@ -475,13 +532,13 @@ curl --cacert --location --request POST 'https://", "platformCaCertFileName": "", "appKeyFileName": "", - "appCertFileName": "" + "appCertFileName": "", } } } ] } -}' \ +}' ``` See the following example command result: @@ -502,7 +559,7 @@ See the following example command result: "appSecretName": "", "logEndpoint": "", "appKeyFileName": "", - "appCertFileName": "" + "appCertFileName": "", } } } @@ -510,7 +567,7 @@ See the following example command result: }, "appInstance": { "status": "DEPLOYING", - "href": "/app-lifecycle-management/v3/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-28057851" + "href": "/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-68129972" } } ``` @@ -520,7 +577,7 @@ Use the App instance ID in the following command to check the instantiation to `"status":"DEPLOYED"`. ```shell -curl --cacert --location --request GET 'https:///app-lifecycle-management/v3/app-instances/' \ +curl --cert --key --cacert --location --request GET 'https:///app-lifecycle-management/v3/app-instances/' \ --header 'accept: application/json' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' @@ -528,41 +585,68 @@ curl --cacert --location --request GET 'https://", - "platformCaCertFileName": "", "iamBaseUrl": "https://", - "appSecretName": "", "logEndpoint": "", + "platformCaCertSecretName": "", + "appSecretName": "", + "platformCaCertFileName": "", "appKeyFileName": "", - "appCertFileName": "" + "appCertFileName": "", }, "namespace": "", "timeout": 5 } + }, + { + "name": "security-mgmt", + "version": "1.0.0", + "type": "SECURITYMANAGEMENT", + "properties": { + "authenticatorType": "client-x509" + } + } + ], + "events": [ + { + "type": "DEPLOY", + "title": "SUCCEEDED", + "detail": "DEPLOY has successfully completed", + "createdAt": "2025-05-31T14:04:16.297Z" + }, + { + "type": "DEPLOY", + "title": "STARTED", + "detail": "DEPLOY has started", + "createdAt": "2025-05-31T14:04:15.609Z" + }, + { + "type": "CREATE", + "title": "SUCCEEDED", + "detail": "CREATE has successfully completed", + "createdAt": "2025-05-31T14:01:01.753Z" } ], - "events": [], "self": { - "href": "/app-lifecycle-management/v3/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-28057851" + "href": "/app-instances/rapp-ericsson-eric-oss-hello-world-python-app-68129972" }, "app": { - "href": "/app-lifecycle-management/v3/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" + "href": "/apps/rapp-ericsson-eric-oss-hello-world-python-app-3-1-1-0" } } ``` @@ -578,7 +662,7 @@ For details, see [Service Exposure - Developer Guide](https://developer.intellig To create an API to be onboarded, run the following commands: ```bash -curl --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis' \ +curl --cert --key --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -604,7 +688,7 @@ To create an endpoint for the previously generated API, run the following command: ```bash -curl --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/endpoints' \ +curl --cert --key --cacert --location --request POST 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/endpoints' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -617,7 +701,7 @@ To bind the plugin for authorization of the previously generated API, run the following command: ```bash -curl --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/phases/auth/plugin-list' \ +curl --cert --key --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/phases/auth/plugin-list' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '[ @@ -631,7 +715,7 @@ To configure the binded plugin for authorization, run the following command: ```bash -curl --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/plugins/requestPartyTokenInterceptor/configuration' \ +curl --cert --key --cacert --location --request PUT 'https:///hub/apiprovisioning/v1/admin/v3/apis/hello-world-python-route-001/plugins/requestPartyTokenInterceptor/configuration' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ @@ -649,7 +733,7 @@ Role-Based Access Control (RBAC) configuration is required. To add the RBAC policy run the following curl command: ```bash -curl --cacert --location --request POST 'https:///idm/rolemgmt/v1/extapp/rbac' \ +curl --cert --key --cacert --location --request POST 'https:///idm/rolemgmt/v1/extapp/rbac' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer ' \ --data '{ diff --git a/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl b/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl index b584d95..1b2f747 100644 --- a/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl +++ b/charts/eric-oss-hello-world-python-app/templates/_helpers.tpl @@ -341,4 +341,21 @@ Define the annotations for security policy */}} {{- define "eric-oss-hello-world-python-app.securityPolicy.annotations" -}} # Automatically generated annotations for documentation purposes. -{{- end -}} \ No newline at end of file +{{- end -}} + +{{/* +Define the function to get the secret name + */}} +{{- define "eric-oss-hello-world-python-app.clientSecret" -}} +{{- $clientSecret := "" -}} +{{- if .Values.global }} + {{- if .Values.global.clientCredentials }} + {{- if .Values.global.clientCredentials.secret }} + {{- if .Values.global.clientCredentials.secret.name }} + {{- $clientSecret = .Values.global.clientCredentials.secret.name }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +{{- print $clientSecret }} +{{- end }} diff --git a/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml b/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml index 511d61e..4aaea3c 100644 --- a/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml +++ b/charts/eric-oss-hello-world-python-app/templates/deployment/deployment.yaml @@ -63,6 +63,10 @@ spec: secret: secretName: {{ index .Values "appSecretName" | quote }} defaultMode: 420 + - name: client-creds + secret: + secretName: {{ include "eric-oss-hello-world-python-app.clientSecret" . | quote }} + defaultMode: 420 containers: - name: eric-oss-hello-world-python-app image: {{ template "eric-oss-hello-world-python-app.imagePath" (dict "imageId" "eric-oss-hello-world-python-app" "values" .Values "files" .Files) }} @@ -89,6 +93,9 @@ spec: - name: app-certs mountPath: {{ index .Values "appCertMountPath" | default .Values.instantiationDefaults.appCertMountPath | quote }} readOnly: true + - name: client-creds + mountPath: {{ index .Values "clientCredsMountPath" | default .Values.instantiationDefaults.clientCredsMountPath | quote }} + readOnly: true env: - name: IAM_CLIENT_ID value: {{ index .Values "clientId" | quote }} @@ -108,6 +115,10 @@ spec: value: {{ index .Values "appCertFileName" | quote }} - name: APP_CERT_FILE_PATH value: {{ index .Values "appCertMountPath" | default .Values.instantiationDefaults.appCertMountPath | quote }} + - name: CLIENT_CREDS_FILE_PATH + value: {{ index .Values "clientCredsMountPath" | default .Values.instantiationDefaults.clientCredsMountPath | quote }} + - name: CLIENT_ID_FILE_NAME + value: {{ .Values.global.clientCredentials.secret.clientIdKey | quote }} - name: SERVICE_NAME value: {{ .Chart.Name }} - name: CONTAINER_NAME diff --git a/charts/eric-oss-hello-world-python-app/values.yaml b/charts/eric-oss-hello-world-python-app/values.yaml index 3acc20f..2c4b392 100644 --- a/charts/eric-oss-hello-world-python-app/values.yaml +++ b/charts/eric-oss-hello-world-python-app/values.yaml @@ -126,3 +126,10 @@ podPriority: instantiationDefaults: platformCaCertMountPath: "/etc/tls-ca/platform/" appCertMountPath: "/etc/tls/log/" + clientCredsMountPath: "/etc/client-creds/" + +global: + clientCredentials: + secret: + clientIdKey: "clientId" + name: "-cc" diff --git a/csar/Definitions/AppDescriptor.yaml b/csar/Definitions/AppDescriptor.yaml index aa9af3f..1dc6c86 100644 --- a/csar/Definitions/AppDescriptor.yaml +++ b/csar/Definitions/AppDescriptor.yaml @@ -3,8 +3,12 @@ Description of an APP: APPName: eric-oss-hello-world-python-app APPVersion: VERSION APPType: rApp -APPComponent: - NameofComponent: eric-oss-hello-world-python-app - Version: VERSION - Path: OtherDefinitions/ASD/eric-oss-hello-world-python-appASD.yaml - ArtefactType: ASD +AppComponentList: + - NameofComponent: eric-oss-hello-world-python-app + Version: VERSION + Path: OtherDefinitions/ASD/eric-oss-hello-world-python-appASD.yaml + ArtefactType: ASD + - NameofComponent: security-mgmt + Version: 1.0.0 + Path: OtherDefinitions/SecurityManagement + ArtefactType: SecurityManagement diff --git a/csar/OtherDefinitions/SecurityManagement/security-metadata.json b/csar/OtherDefinitions/SecurityManagement/security-metadata.json new file mode 100644 index 0000000..cd02ad6 --- /dev/null +++ b/csar/OtherDefinitions/SecurityManagement/security-metadata.json @@ -0,0 +1,3 @@ +{ + "authenticatorType": "client-x509" +} diff --git a/eric-oss-hello-world-python-app/config.py b/eric-oss-hello-world-python-app/config.py index e9bb85b..603fa5e 100644 --- a/eric-oss-hello-world-python-app/config.py +++ b/eric-oss-hello-world-python-app/config.py @@ -1,38 +1,40 @@ -'''This module handles environment variables''' +"""This module handles environment variables""" + import os + def get_config(): - ''' - get env and return config with all env vals required - ''' - iam_client_id = get_os_env_string("IAM_CLIENT_ID", "") - iam_client_secret = get_os_env_string("IAM_CLIENT_SECRET", "") - iam_base_url = get_os_env_string("IAM_BASE_URL", "") - ca_cert_file_name = get_os_env_string("CA_CERT_FILE_NAME", "") - ca_cert_file_path = get_os_env_string("CA_CERT_FILE_PATH", "") - log_ctrl_file = get_os_env_string("LOG_CTRL_FILE", "") - log_endpoint = get_os_env_string("LOG_ENDPOINT", "") - app_key = get_os_env_string("APP_KEY", "") - app_cert = get_os_env_string("APP_CERT", "") - app_cert_file_path = get_os_env_string("APP_CERT_FILE_PATH", "") + """get env and return config with all env vals required""" + iam_client_id = get_os_env_string("IAM_CLIENT_ID", "") + iam_client_secret = get_os_env_string("IAM_CLIENT_SECRET", "") + iam_base_url = get_os_env_string("IAM_BASE_URL", "") + ca_cert_file_name = get_os_env_string("CA_CERT_FILE_NAME", "") + ca_cert_file_path = get_os_env_string("CA_CERT_FILE_PATH", "") + log_ctrl_file = get_os_env_string("LOG_CTRL_FILE", "") + log_endpoint = get_os_env_string("LOG_ENDPOINT", "") + app_key = get_os_env_string("APP_KEY", "") + app_cert = get_os_env_string("APP_CERT", "") + app_cert_file_path = get_os_env_string("APP_CERT_FILE_PATH", "") + client_creds_file_path = get_os_env_string("CLIENT_CREDS_FILE_PATH", "") + client_id_file_name = get_os_env_string("CLIENT_ID_FILE_NAME", "") config = { - "iam_client_id": iam_client_id, - "iam_client_secret": iam_client_secret, - "iam_base_url": iam_base_url, - "ca_cert_file_name": ca_cert_file_name, - "ca_cert_file_path": ca_cert_file_path, - "log_ctrl_file": log_ctrl_file, - "log_endpoint": log_endpoint, - "app_key": app_key, - "app_cert": app_cert, - "app_cert_file_path": app_cert_file_path + "iam_client_id": iam_client_id, + "iam_client_secret": iam_client_secret, + "iam_base_url": iam_base_url, + "ca_cert_file_name": ca_cert_file_name, + "ca_cert_file_path": ca_cert_file_path, + "log_ctrl_file": log_ctrl_file, + "log_endpoint": log_endpoint, + "app_key": app_key, + "app_cert": app_cert, + "app_cert_file_path": app_cert_file_path, + "client_creds_file_path": client_creds_file_path, + "client_id_file_name": client_id_file_name, } return config def get_os_env_string(env_name, default_value): - ''' - get env - ''' + """get env""" return os.getenv(env_name, default_value).strip() diff --git a/eric-oss-hello-world-python-app/login.py b/eric-oss-hello-world-python-app/login.py index 4112a04..4da2e89 100644 --- a/eric-oss-hello-world-python-app/login.py +++ b/eric-oss-hello-world-python-app/login.py @@ -1,52 +1,65 @@ -''' +""" This module performs client credentials grant authentication by sending HTTP requests with TLS and with required environment variables. - ''' + """ + import os from urllib.parse import urljoin import json +import time import requests from config import get_config + class LoginError(Exception): """Raised when EIC login fails""" def login(): - ''' + """ Get bearer token for accessing platform REST APIs: https://developer.intelligentautomationplatform.ericsson.net/#tutorials/app-authentication - ''' + """ config = get_config() login_path = "/auth/realms/master/protocol/openid-connect/token" login_url = urljoin(config.get("iam_base_url"), login_path) - headers = { - "Content-Type": "application/x-www-form-urlencoded" - } - form_data = { - "grant_type": "client_credentials", - "client_id": config.get("iam_client_id"), - "client_secret": config.get("iam_client_secret"), - "tenant_id": "master" - } - try: - resp = tls_login(login_url, form_data, headers) - except LoginError: - return None, 0 - - resp = json.loads(resp.decode('utf-8')) + headers = {"Content-Type": "application/x-www-form-urlencoded"} + resp = tls_login(login_url, headers) + resp = json.loads(resp.decode("utf-8")) token, time_until_expiry = resp["access_token"], resp["expires_in"] - time_until_expiry -= 10 # add a buffer to ensure our session doesn't expire mid-request - return token, time_until_expiry + time_until_expiry -= ( + 10 # add a buffer to ensure our session doesn't expire mid-request + ) + return token, time.time() + time_until_expiry -def tls_login(url, form_data, headers): - ''' - This function sends an HTTP POST request with TLS for the login operation - ''' + +def tls_login(url, headers): + """This function sends an HTTP POST request with TLS for the login operation""" config = get_config() - cert = os.path.join("/", config.get("ca_cert_file_path"), config.get("ca_cert_file_name")) + ca_cert = os.path.join( + "/", config.get("ca_cert_file_path"), config.get("ca_cert_file_name") + ) + app_cert = os.path.join( + "/", config.get("app_cert_file_path"), config.get("app_cert") + ) + app_key = os.path.join( + "/", config.get("app_cert_file_path"), config.get("app_key")) + client_id_path = os.path.join( + "/", config.get("client_creds_file_path"), config.get("client_id_file_name") + ) + form_data = {"grant_type": "client_credentials", "tenant_id": "master"} + cert = (app_cert, app_key) + + try: + with open(client_id_path, "r") as f: + form_data["client_id"] = f.read().strip() + except OSError as e: + raise LoginError(f"Error while reading client id: {e}") + try: - response = requests.post(url, data=form_data, headers = headers, timeout=5, verify=cert) + response = requests.post( + url, data=form_data, headers=headers, timeout=5, verify=ca_cert, cert=cert + ) if response.status_code != 200: raise LoginError(f"Login failed ({response.status_code})") except Exception as exception: diff --git a/eric-oss-hello-world-python-app/main.py b/eric-oss-hello-world-python-app/main.py index 2ce8e11..8d3611b 100755 --- a/eric-oss-hello-world-python-app/main.py +++ b/eric-oss-hello-world-python-app/main.py @@ -1,46 +1,55 @@ #!/usr/bin/env python3 -''' +""" Flask Application for Hello World Service This Python script defines a Flask application that implements a simple "Hello World" service along with a health check and metrics endpoints. -''' +""" import time from flask import abort from flask import Flask from login import login from mtls_logging import MtlsLogging, Severity from werkzeug.middleware.dispatcher import DispatcherMiddleware -from prometheus_client import disable_created_metrics, make_wsgi_app, CollectorRegistry, Counter +from prometheus_client import ( + disable_created_metrics, + make_wsgi_app, + CollectorRegistry, + Counter, +) SERVICE_PREFIX = "python_hello_world" class Application(Flask): - '''The Flask application itself. Subclassed for testing.''' + """The Flask application itself. Subclassed for testing.""" def __init__(self): super().__init__(__name__) disable_created_metrics() self.counters = {"total_failed": 0, "total_requests": 0} self.session = {"token": None, "expiry_time": 0} self.create_metrics() - self.wsgi_app = DispatcherMiddleware(self.wsgi_app, { - '/sample-app/python/metrics': make_wsgi_app(registry=self.registry) - }) + self.wsgi_app = DispatcherMiddleware( + self.wsgi_app, + {"/sample-app/python/metrics": make_wsgi_app(registry=self.registry)}, + ) self.logger = MtlsLogging() @self.route("/sample-app/python/") def root(): - '''This route returns a 400 Bad Request HTTP response.''' - self.logger.log("400 Bad request: User tried accessing '/sample-app/python/'", Severity.INFO) + """This route returns a 400 Bad Request HTTP response.""" + self.logger.log( + "400 Bad request: User tried accessing '/sample-app/python/'", + Severity.INFO, + ) abort(400) @self.route("/sample-app/python/hello") def hello(): - ''' + """ This route performs a login operation and returns a simple "Hello World!" greeting and increments the total request counter. - ''' + """ self.update_session() self.requests_total.inc() self.logger.log("200 OK: Hello World!", Severity.INFO) @@ -48,35 +57,40 @@ def hello(): @self.route("/sample-app/python/health") def health(): - ''' + """ This route provides a simple health check endpoint, returning "Ok" to indicate that the application is healthy. - ''' + """ self.update_session() self.logger.log("200 OK: Health check", Severity.INFO) return "Ok\n" - def update_session(self): - '''Refresh session if it expires.''' + """Refresh session if it expires.""" if int(time.time()) >= self.session["expiry_time"]: - self.session["token"], self.session["expiry_time"] = login() - if not self.session["token"]: - # since the token isn't used for anything, - # this is just a WARNING level log instead of ERROR - self.logger.log("Login failed", Severity.WARNING) + try: + self.session["token"], self.session["expiry_time"] = login() + except Exception as e: + # since the token isn't used for anything, + # this is just a WARNING level log instead of ERROR + self.logger.log(f"Login failed: {e}", Severity.WARNING) + def create_metrics(self): self.registry = CollectorRegistry() - self.requests_total = Counter(namespace=SERVICE_PREFIX, - name="requests_total", - documentation="Total number of API requests") - self.requests_failed = Counter(namespace=SERVICE_PREFIX, - name="requests_failed_total", - documentation="Total number of API request failures") + self.requests_total = Counter( + namespace=SERVICE_PREFIX, + name="requests_total", + documentation="Total number of API requests", + ) + self.requests_failed = Counter( + namespace=SERVICE_PREFIX, + name="requests_failed_total", + documentation="Total number of API request failures", + ) self.registry.register(self.requests_total) self.registry.register(self.requests_failed) -if __name__ == '__main__': +if __name__ == "__main__": instance = Application() - instance.run(host = '0.0.0.0', port = '8050') + instance.run(host="0.0.0.0", port="8050") diff --git a/eric-oss-hello-world-python-app/mtls_logging.py b/eric-oss-hello-world-python-app/mtls_logging.py index dc91c70..47fc012 100644 --- a/eric-oss-hello-world-python-app/mtls_logging.py +++ b/eric-oss-hello-world-python-app/mtls_logging.py @@ -1,5 +1,4 @@ -'''This module handles mTLS logging''' - +"""This module handles mTLS logging""" import json import os import logging @@ -11,18 +10,18 @@ class Severity(IntEnum): - '''We use this to map the logging library severity to the mTLS logging''' + """We use this to map the logging library severity to the mTLS logging""" DEBUG = 10 INFO = 20 WARNING = 30 ERROR = 40 CRITICAL = 50 -#pylint: disable=too-few-public-methods +# pylint: disable=too-few-public-methods class MtlsLogging: - '''mTLS logger which will log to STDOUT, as well as Log Aggregator''' + """mTLS logger which will log to STDOUT, as well as Log Aggregator""" def __init__(self, level=None): - werkzeug_logger = logging.getLogger('werkzeug') + werkzeug_logger = logging.getLogger("werkzeug") werkzeug_logger.setLevel(logging.ERROR) self.config = get_config() self.logger = logging.getLogger(__name__) @@ -35,7 +34,9 @@ def __init__(self, level=None): level = Severity.INFO if self.config["log_ctrl_file"]: # If level is defined in charts\eric-oss-hello-world-python-app\logcontrol.json - with open(self.config["log_ctrl_file"], "r", encoding="utf-8") as log_ctrl_file: + with open( + self.config["log_ctrl_file"], "r", encoding="utf-8" + ) as log_ctrl_file: log_ctrl = json.load(log_ctrl_file) container_name = get_os_env_string("CONTAINER_NAME", "") for obj in log_ctrl: @@ -54,31 +55,27 @@ def __init__(self, level=None): self.logger.addHandler(handler) self.log(f"Level set to: {level}", Severity.INFO) - def log(self, message, severity): - ''' - Send request to log aggregator with mTLS - ''' - + """Send request to log aggregator with mTLS""" # Determine if certs are set. - cert_available = (self.config.get("ca_cert_file_name") != "" - and self.config.get("ca_cert_file_path") != "" - and self.config.get("app_cert") != "" - and self.config.get("app_key") != "" - and self.config.get("app_cert_file_path") != "") + cert_available = ( + self.config.get("ca_cert_file_name") != "" + and self.config.get("ca_cert_file_path") != "" + and self.config.get("app_cert") != "" + and self.config.get("app_key") != "" + and self.config.get("app_cert_file_path") != "" + ) log_url = self.config.get("log_endpoint") time = datetime.now(timezone.utc).isoformat() - headers = { - "Content-Type": "application/json" - } + headers = {"Content-Type": "application/json"} json_data = { "timestamp": time, "version": "0.0.1", "message": message, "service_id": "rapp-eric-oss-hello-world-python-app", - "severity": severity.name.lower() + "severity": severity.name.lower(), } # print to console @@ -94,11 +91,32 @@ def log(self, message, severity): elif severity >= self.logger.getEffectiveLevel(): # send log to log transformer try: - ca_cert = os.path.join("/", self.config.get("ca_cert_file_path"), self.config.get("ca_cert_file_name")) - app_cert = os.path.join("/", self.config.get("app_cert_file_path"), self.config.get("app_cert")) - app_key = os.path.join("/", self.config.get("app_cert_file_path"), self.config.get("app_key")) - requests.post(f"https://{log_url}", json=json_data, timeout=5, - headers = headers, verify=ca_cert, cert=(app_cert, app_key)) - except (requests.exceptions.InvalidURL, requests.exceptions.MissingSchema) as exception: + ca_cert = os.path.join( + "/", + self.config.get("ca_cert_file_path"), + self.config.get("ca_cert_file_name"), + ) + app_cert = os.path.join( + "/", + self.config.get("app_cert_file_path"), + self.config.get("app_cert"), + ) + app_key = os.path.join( + "/", + self.config.get("app_cert_file_path"), + self.config.get("app_key"), + ) + requests.post( + f"https://{log_url}", + json=json_data, + timeout=5, + headers=headers, + verify=ca_cert, + cert=(app_cert, app_key), + ) + except ( + requests.exceptions.InvalidURL, + requests.exceptions.MissingSchema, + ) as exception: # logs to console if failed to log to log transformer self.logger.error("Request failed for mTLS logging: %s", exception) diff --git a/eric-oss-hello-world-python-app/tests/client_id_example b/eric-oss-hello-world-python-app/tests/client_id_example new file mode 100644 index 0000000..7f6e5ba --- /dev/null +++ b/eric-oss-hello-world-python-app/tests/client_id_example @@ -0,0 +1 @@ +IAM_CLIENT_ID \ No newline at end of file diff --git a/eric-oss-hello-world-python-app/tests/conftest.py b/eric-oss-hello-world-python-app/tests/conftest.py index 8206429..c5cda80 100644 --- a/eric-oss-hello-world-python-app/tests/conftest.py +++ b/eric-oss-hello-world-python-app/tests/conftest.py @@ -1,17 +1,17 @@ -''' -Configure a Flask fixture based off the Application defined in main.py -''' - +"""Configure a Flask fixture based off the Application defined in main.py""" import os +from urllib.parse import urljoin import pytest import requests_mock from prometheus_client import REGISTRY as GLOBAL_METRICS_REGISTRY from main import Application from config import get_config + def pytest_generate_tests(): populate_environment_variables() + @pytest.fixture(name="mock_log_api") def fixture_mock_log_api(config): log_endpoint = f"https://{config.get('log_endpoint')}" @@ -20,53 +20,104 @@ def fixture_mock_log_api(config): yield request_mocker +def match_request_data(request): + uses_x509 = request.cert and all( + [ + parameter in request.text + for parameter in [ + "grant_type=client_credentials", + "tenant_id=master", + "client_id=IAM_CLIENT_ID", + ] + ] + ) + uses_legacy = all( + [ + parameter in request.text + for parameter in [ + "grant_type=client_credentials", + "tenant_id=master", + "client_id=IAM_CLIENT_ID", + "client_secret=IAM_CLIENT_SECRET", + ] + ] + ) + return uses_x509 or uses_legacy + + +@pytest.fixture(name="mock_login_api") +def fixture_mock_login_api(config): + login_endpoint = urljoin( + config.get("iam_base_url"), "/auth/realms/master/protocol/openid-connect/token" + ) + with requests_mock.Mocker() as request_mocker: + request_mocker.post( + login_endpoint, + request_headers={"Content-Type": "application/x-www-form-urlencoded"}, + additional_matcher=match_request_data, + json={ + "access_token": "2YotnFZFEjr1zCsicMWpAA", + "token_type": "example", + "expires_in": 3600, + "example_parameter": "example_value", + }, + ) # Example reply from OAuth2 spec: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3 + yield request_mocker + + @pytest.fixture(name="app") def fixture_app(mock_log_api): # pylint: disable=unused-argument - '''Create a fixture out of our Application, which will be used by any test_*.py file''' + """Create a fixture out of our Application, which will be used by any test_*.py file""" application = Application() - application.config.update({ - "TESTING": True, - }) + application.config.update( + { + "TESTING": True, + } + ) # Why 'yield'? See: https://docs.pytest.org/en/7.1.x/how-to/fixtures.html#dynamic-scope - yield application + yield application GLOBAL_METRICS_REGISTRY.unregister(application.requests_total) GLOBAL_METRICS_REGISTRY.unregister(application.requests_failed) @pytest.fixture() def client(app): - '''Every time a test wants a client, give it a new copy of our Application''' + """Every time a test wants a client, give it a new copy of our Application""" return app.test_client() @pytest.fixture(name="config") def fixture_config(): - '''Every time a test wants a config, give it a stub''' + """Every time a test wants a config, give it a stub""" return get_config() + @pytest.fixture(scope="function") def no_log_certs(): # Remove references to log certs to simulate them being undefined. # This would simulate a user not setting these at instantiation time. - os.environ["APP_KEY"] = "" - os.environ["APP_CERT"] = "" - os.environ["APP_CERT_FILE_PATH"] = "" - + os.environ["APP_KEY"] = "" + os.environ["APP_CERT"] = "" + os.environ["APP_CERT_FILE_PATH"] = "" + # Why 'yield'? See: https://docs.pytest.org/en/7.1.x/how-to/fixtures.html#dynamic-scope yield populate_environment_variables() + def populate_environment_variables(): - os.environ["IAM_CLIENT_ID"] = "IAM_CLIENT_ID" - os.environ["IAM_CLIENT_SECRET"] = "IAM_CLIENT_SECRET" - os.environ["IAM_BASE_URL"] = "https://www.iam-base-url.com" - os.environ["CA_CERT_FILE_NAME"] = "CA_CERT_FILE_NAME" - os.environ["CA_CERT_FILE_PATH"] = "CA_CERT_MOUNT_PATH" - os.environ["LOG_ENDPOINT"] = "LOG_ENDPOINT" - os.environ["APP_KEY"] = "APP_KEY" - os.environ["APP_CERT"] = "APP_CERT" - os.environ["APP_CERT_FILE_PATH"] = "APP_CERT_FILE_PATH" \ No newline at end of file + os.environ["IAM_CLIENT_ID"] = "IAM_CLIENT_ID" + os.environ["IAM_CLIENT_SECRET"] = "IAM_CLIENT_SECRET" + os.environ["IAM_BASE_URL"] = "https://www.iam-base-url.com" + os.environ["CA_CERT_FILE_NAME"] = "CA_CERT_FILE_NAME" + os.environ["CA_CERT_FILE_PATH"] = "CA_CERT_MOUNT_PATH" + os.environ["LOG_ENDPOINT"] = "LOG_ENDPOINT" + os.environ["APP_KEY"] = "APP_KEY" + os.environ["APP_CERT"] = "APP_CERT" + os.environ["APP_CERT_FILE_PATH"] = "APP_CERT_FILE_PATH" + os.environ["CLIENT_CREDS_FILE_PATH"] = os.path.relpath(os.path.dirname(__file__), "/") + os.environ["CLIENT_ID_FILE_NAME"] = "client_id_example" diff --git a/eric-oss-hello-world-python-app/tests/test_login.py b/eric-oss-hello-world-python-app/tests/test_login.py index d0e32d1..6b0ffab 100644 --- a/eric-oss-hello-world-python-app/tests/test_login.py +++ b/eric-oss-hello-world-python-app/tests/test_login.py @@ -1,26 +1,23 @@ -'''Tests which ensure the application handles Authentication & Authorisation properly''' +"""Tests which ensure the application handles Authentication & Authorisation properly""" from urllib.parse import urljoin -from login import login +import time +from login import login, LoginError +import pytest - -def test_login_receives_token(requests_mock, config): - '''Check if we receive a token''' - login_url = urljoin(config.get("iam_base_url"), "/auth/realms/master/protocol/openid-connect/token") - requests_mock.post(login_url, json = { - "access_token":"2YotnFZFEjr1zCsicMWpAA", - "token_type":"example", - "expires_in":3600, - "example_parameter":"example_value" - }) # Example reply from OAuth2 spec: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4.3 +def test_login_receives_token_x509(mock_login_api, config): + """Check if we receive a token""" token, expiry = login() - assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry + 10 == 3600 + assert token == "2YotnFZFEjr1zCsicMWpAA" and expiry > time.time() def test_login_bad_credentials(requests_mock, config): - '''Ensure we get an error if credentials are bad''' - login_url = urljoin(config.get("iam_base_url"), "/auth/realms/master/protocol/openid-connect/token") - requests_mock.post(login_url, status_code=400, json = { - "error":"invalid_request" - }) # Example reply from OAuth2 spec: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 - token, expiry = login() - assert not token and expiry == 0 + """Ensure we get an error if credentials are bad""" + login_url = urljoin( + config.get("iam_base_url"), "/auth/realms/master/protocol/openid-connect/token" + ) + requests_mock.post( + login_url, status_code=400, json={"error": "invalid_request"} + ) # Example reply from OAuth2 spec: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + with pytest.raises(LoginError) as e: + login() + assert "400" in e diff --git a/eric-oss-hello-world-python-app/tests/test_main.py b/eric-oss-hello-world-python-app/tests/test_main.py index 994731b..d99baab 100644 --- a/eric-oss-hello-world-python-app/tests/test_main.py +++ b/eric-oss-hello-world-python-app/tests/test_main.py @@ -1,62 +1,66 @@ -'''Tests which cover the routes of the application''' +"""Tests which cover the routes of the application""" + def test_get_root_returns_bad_response(client): - ''' + """ GET to "/" 400 Bad Request - ''' + """ response = client.get("/sample-app/python/") assert response.status_code == 400 def test_get_hello_returns_hello_world(client): - ''' + """ GET to "/hello" 200 OK Body "Hello World!\n" - ''' + """ response = client.get("/sample-app/python/hello") assert [response.text, response.status_code] == ["Hello World!\n", 200] def test_get_metrics_returns_metrics(client): - ''' + """ GET to "/metrics" 200 OK Body containing Prometheus-compatible metrics - ''' + """ response = client.get("/sample-app/python/metrics") assert response.status_code == 200 assert "python_hello_world_requests_total 0.0" in response.text assert "python_hello_world_requests_failed_total 0.0" in response.text + def test_metrics_does_not_expose_created(client): - ''' + """ GET to "/metrics" 200 OK Body does not contain _created gauges for Prometheus-compatible metrics - ''' + """ response = client.get("/sample-app/python/metrics") assert response.status_code == 200 assert "_created" not in response.text + def test_metrics_successfully_increments(client): - ''' + """ GET to "/metrics" 200 OK Body containing metrics which have incremented by 1 - ''' + """ client.get("/sample-app/python/hello") response = client.get("/sample-app/python/metrics") assert response.status_code == 200 assert "python_hello_world_requests_total 1.0" in response.text assert "python_hello_world_requests_failed_total 0.0" in response.text + def test_get_health_returns_health_check(client): - ''' + """ GET to "/health" 200 OK Body "Ok\n" - ''' + """ response = client.get("/sample-app/python/health") assert [response.text, response.status_code] == ["Ok\n", 200] diff --git a/eric-oss-hello-world-python-app/tests/test_mtls_logging.py b/eric-oss-hello-world-python-app/tests/test_mtls_logging.py index a5c9091..42cffdc 100644 --- a/eric-oss-hello-world-python-app/tests/test_mtls_logging.py +++ b/eric-oss-hello-world-python-app/tests/test_mtls_logging.py @@ -1,27 +1,33 @@ -'''Tests which cover the app's logging, both to STDOUT and to Log Aggregator''' +"""Tests which cover the app's logging, both to STDOUT and to Log Aggregator""" + from unittest import mock import requests from mtls_logging import MtlsLogging, Severity def test_log_stdout_and_mtls(caplog): - '''Ensure any log is sent both to STDOUT and through HTTPS''' + """Ensure any log is sent both to STDOUT and through HTTPS""" message = "Message which should appear in both STDOUT and sent as a POST request" - with_mocked_post(send_log, message, Severity.DEBUG, Severity.CRITICAL).assert_called() + with_mocked_post( + send_log, message, Severity.DEBUG, Severity.CRITICAL + ).assert_called() assert message in caplog.text + def test_log_stdout_and_not_mtls(no_log_certs, caplog): # pylint: disable=unused-argument - '''Ensure log is only sent to STDOUT when missing log certs''' + """Ensure log is only sent to STDOUT when missing log certs""" message = "Message which should appear in STDOUT" - error_message = ("Missing TLS logging additional parameter(s): log_ctrl_file app_key app_cert app_cert_file_path") - with_mocked_post(send_log, message, Severity.DEBUG, Severity.CRITICAL).assert_not_called() + error_message = "Missing TLS logging additional parameter(s): log_ctrl_file app_key app_cert app_cert_file_path" + with_mocked_post( + send_log, message, Severity.DEBUG, Severity.CRITICAL + ).assert_not_called() assert message in caplog.text assert error_message in caplog.text def test_log_level_matching_severity(caplog): - '''Ensure a log will output if it matches the severity level of the logger''' + """Ensure a log will output if it matches the severity level of the logger""" for severity in Severity: message = f"Message sent with severity {severity}, should be logged in STDOUT and through POST" with_mocked_post(send_log, message, severity, severity).assert_called() @@ -29,17 +35,20 @@ def test_log_level_matching_severity(caplog): def test_log_ignored(caplog): - '''Ensure that a log will be ignored if we set the minimum severity higher''' + """Ensure that a log will be ignored if we set the minimum severity higher""" message = "Message which should appear in both STDOUT and sent as a POST request" # This test will still call once because the logger announces its log level as INFO severity - with_mocked_post(send_log, message, Severity.INFO, Severity.DEBUG).assert_called_once() + with_mocked_post( + send_log, message, Severity.INFO, Severity.DEBUG + ).assert_called_once() assert not message in caplog.text ################################### HELPERS ################################### + def with_mocked_post(log_function, message, logger_level, log_level): - '''Send a log with mocked POST request, ensure the request is called''' + """Send a log with mocked POST request, ensure the request is called""" with mock.patch.object(requests, "post") as mock_post: mock_post.return_value.status_code = 201 mock_post.return_value.text = message @@ -48,6 +57,6 @@ def with_mocked_post(log_function, message, logger_level, log_level): def send_log(message, logger_level, log_level): - '''Send a log through the MTLS logger''' + """Send a log through the MTLS logger""" logger = MtlsLogging(logger_level) logger.log(message, log_level) diff --git a/requirements.txt b/requirements.txt index f37ed8d..b9f5b91 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ flask==3.0.1 -requests==2.32.0 -prometheus-client==0.20.0 \ No newline at end of file +requests==2.32.4 +prometheus-client==0.20.0 diff --git a/version b/version index acf9bf0..0c89fc9 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.2.2 \ No newline at end of file +4.0.0 \ No newline at end of file