diff --git a/manifests/kpi_value_apiservice.yaml b/manifests/kpi_value_apiservice.yaml new file mode 100644 index 0000000000000000000000000000000000000000..eb53a23839b7c452f848c1819f4ba3cf620eed2e --- /dev/null +++ b/manifests/kpi_value_apiservice.yaml @@ -0,0 +1,99 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kpi-value-apiservice +spec: + selector: + matchLabels: + app: kpi-value-apiservice + #replicas: 1 + template: + metadata: + annotations: + config.linkerd.io/skip-outbound-ports: "4222" + labels: + app: kpi-value-apiservice + spec: + terminationGracePeriodSeconds: 5 + containers: + - name: server + image: labs.etsi.org:5050/tfs/controller/kpi_manager:latest + imagePullPolicy: Always + ports: + - containerPort: 30020 + - containerPort: 9192 + env: + - name: LOG_LEVEL + value: "INFO" + # envFrom: + # - secretRef: + # name: crdb-data + readinessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:30020"] + livenessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:30020"] + resources: + requests: + cpu: 250m + memory: 128Mi + limits: + cpu: 1000m + memory: 1024Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: kpi-value-apiservice + labels: + app: kpi-value-apiservice +spec: + type: ClusterIP + selector: + app: kpi-value-apiservice + ports: + - name: grpc + protocol: TCP + port: 30020 + targetPort: 30020 + - name: metrics + protocol: TCP + port: 9192 + targetPort: 9192 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: kpi-value-apiservice-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: kpi-value-apiservice + minReplicas: 1 + maxReplicas: 20 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 80 + #behavior: + # scaleDown: + # stabilizationWindowSeconds: 30 diff --git a/manifests/kpi_value_writerservice.yaml b/manifests/kpi_value_writerservice.yaml new file mode 100644 index 0000000000000000000000000000000000000000..19cab536f04dbfade01464350b0f8dd1961e9516 --- /dev/null +++ b/manifests/kpi_value_writerservice.yaml @@ -0,0 +1,99 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kpi-value-writerservice +spec: + selector: + matchLabels: + app: kpi-value-writerservice + #replicas: 1 + template: + metadata: + annotations: + config.linkerd.io/skip-outbound-ports: "4222" + labels: + app: kpi-value-writerservice + spec: + terminationGracePeriodSeconds: 5 + containers: + - name: server + image: labs.etsi.org:5050/tfs/controller/kpi_manager:latest + imagePullPolicy: Always + ports: + - containerPort: 30030 + - containerPort: 9192 + env: + - name: LOG_LEVEL + value: "INFO" + envFrom: + - secretRef: + name: crdb-data + readinessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:30030"] + livenessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:30030"] + resources: + requests: + cpu: 250m + memory: 128Mi + limits: + cpu: 1000m + memory: 1024Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: kpi-value-writerservice + labels: + app: kpi-value-writerservice +spec: + type: ClusterIP + selector: + app: kpi-value-writerservice + ports: + - name: grpc + protocol: TCP + port: 30030 + targetPort: 30030 + - name: metrics + protocol: TCP + port: 9192 + targetPort: 9192 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: kpi-value-writerservice-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: kpi-value-writerservice + minReplicas: 1 + maxReplicas: 20 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 80 + #behavior: + # scaleDown: + # stabilizationWindowSeconds: 30 diff --git a/src/common/Constants.py b/src/common/Constants.py index 8f3e7894dca5d6185cdbc19b42669b0e5ea98e8b..767b21343f89e35c2338b522bcdc71c56aca1815 100644 --- a/src/common/Constants.py +++ b/src/common/Constants.py @@ -43,9 +43,6 @@ class ServiceNameEnum(Enum): ZTP = 'ztp' POLICY = 'policy' MONITORING = 'monitoring' - KPIMANAGER = 'kpi-manager' - KPIVALUEAPI = 'kpi-value-api' - TELEMETRYFRONTEND = 'telemetry-frontend' DLT = 'dlt' NBI = 'nbi' CYBERSECURITY = 'cybersecurity' @@ -64,6 +61,10 @@ class ServiceNameEnum(Enum): E2EORCHESTRATOR = 'e2e-orchestrator' OPTICALCONTROLLER = 'opticalcontroller' BGPLS = 'bgpls-speaker' + KPIMANAGER = 'kpi-manager' + KPIVALUEAPI = 'kpi-value-api' + KPIVALUEWRITER = 'kpi-value-writer' + TELEMETRYFRONTEND = 'telemetry-frontend' # Used for test and debugging only DLT_GATEWAY = 'dltgateway' @@ -95,6 +96,7 @@ DEFAULT_SERVICE_GRPC_PORTS = { ServiceNameEnum.BGPLS .value : 20030, ServiceNameEnum.KPIMANAGER .value : 30010, ServiceNameEnum.KPIVALUEAPI .value : 30020, + ServiceNameEnum.KPIVALUEWRITER .value : 30030, ServiceNameEnum.TELEMETRYFRONTEND .value : 30050, # Used for test and debugging only diff --git a/src/kpi_manager/.gitlab-ci.yml b/src/kpi_manager/.gitlab-ci.yml index ffd4e38ffabd0d18b85a86bca7760001a5466b73..6aef328ea51e835eb06b286478f26d83f9a80f13 100644 --- a/src/kpi_manager/.gitlab-ci.yml +++ b/src/kpi_manager/.gitlab-ci.yml @@ -13,9 +13,9 @@ # limitations under the License. # Build, tag, and push the Docker image to the GitLab Docker registry -build kpi_manager: +build kpi-manager: variables: - IMAGE_NAME: 'kpi_manager' # name of the microservice + IMAGE_NAME: 'kpi-manager' # name of the microservice IMAGE_TAG: 'latest' # tag of the container image (production, development, etc) stage: build before_script: @@ -39,13 +39,13 @@ build kpi_manager: - .gitlab-ci.yml # Apply unit test to the component -unit_test context: +unit_test kpi-manager: variables: - IMAGE_NAME: 'kpi_manager' # name of the microservice + IMAGE_NAME: 'kpi-manager' # name of the microservice IMAGE_TAG: 'latest' # tag of the container image (production, development, etc) stage: unit_test needs: - - build context + - build kpi-manager before_script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - if docker network list | grep teraflowbridge; then echo "teraflowbridge is already created"; else docker network create -d bridge teraflowbridge; fi @@ -71,7 +71,7 @@ unit_test context: - NATS_ADDRESS=$(docker inspect nats --format "{{.NetworkSettings.Networks.teraflowbridge.IPAddress}}") - echo $NATS_ADDRESS - > - docker run --name $IMAGE_NAME -d -p 1010:1010 + docker run --name $IMAGE_NAME -d -p 30010:30010 --env "CRDB_URI=cockroachdb://tfs:tfs123@${CRDB_ADDRESS}:26257/tfs_test?sslmode=require" --volume "$PWD/src/$IMAGE_NAME/tests:/opt/results" --network=teraflowbridge diff --git a/src/kpi_value_api/.gitlab-ci.yml b/src/kpi_value_api/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..c9107abaac83199cc18ea720e4e3e5e14e353189 --- /dev/null +++ b/src/kpi_value_api/.gitlab-ci.yml @@ -0,0 +1,109 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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. + +# Build, tag, and push the Docker image to the GitLab Docker registry +build kpi-value-api: + variables: + IMAGE_NAME: 'kpi-value-api' # name of the microservice + IMAGE_TAG: 'latest' # tag of the container image (production, development, etc) + stage: build + before_script: + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + script: + - docker buildx build -t "$IMAGE_NAME:$IMAGE_TAG" -f ./src/$IMAGE_NAME/Dockerfile . + - docker tag "$IMAGE_NAME:$IMAGE_TAG" "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" + - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" + after_script: + - docker images --filter="dangling=true" --quiet | xargs -r docker rmi + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' + - changes: + - src/common/**/*.py + - proto/*.proto + - src/$IMAGE_NAME/**/*.{py,in,yml} + - src/$IMAGE_NAME/Dockerfile + - src/$IMAGE_NAME/tests/*.py + - manifests/${IMAGE_NAME}service.yaml + - .gitlab-ci.yml + +# Apply unit test to the component +unit_test kpi-value-api: + variables: + IMAGE_NAME: 'kpi-value-api' # name of the microservice + IMAGE_TAG: 'latest' # tag of the container image (production, development, etc) + stage: unit_test + needs: + - build kpi-value-api + before_script: + - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + - if docker network list | grep teraflowbridge; then echo "teraflowbridge is already created"; else docker network create -d bridge teraflowbridge; fi + - if docker container ls | grep $IMAGE_NAME; then docker rm -f $IMAGE_NAME; else echo "$IMAGE_NAME container is not in the system"; fi + - docker container prune -f + script: + - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" + - docker run --name $IMAGE_NAME -d -p 30020:30020 -v "$PWD/src/$IMAGE_NAME/tests:/opt/results" --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG + - sleep 5 + - docker ps -a + - docker logs $IMAGE_NAME + - > + docker exec -i $IMAGE_NAME bash -c + "coverage run -m pytest --log-level=INFO --verbose --junitxml=/opt/results/${IMAGE_NAME}_report.xml $IMAGE_NAME/tests/test_*.py" + - docker exec -i $IMAGE_NAME bash -c "coverage report --include='${IMAGE_NAME}/*' --show-missing" + coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' + after_script: + - docker rm -f $IMAGE_NAME + - docker network rm teraflowbridge + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' + - changes: + - src/common/**/*.py + - proto/*.proto + - src/$IMAGE_NAME/**/*.{py,in,yml} + - src/$IMAGE_NAME/Dockerfile + - src/$IMAGE_NAME/tests/*.py + - src/$IMAGE_NAME/tests/Dockerfile + - manifests/${IMAGE_NAME}service.yaml + - .gitlab-ci.yml + artifacts: + when: always + reports: + junit: src/$IMAGE_NAME/tests/${IMAGE_NAME}_report.xml + +## Deployment of the service in Kubernetes Cluster +#deploy context: +# variables: +# IMAGE_NAME: 'context' # name of the microservice +# IMAGE_TAG: 'latest' # tag of the container image (production, development, etc) +# stage: deploy +# needs: +# - unit test context +# # - integ_test execute +# script: +# - 'sed -i "s/$IMAGE_NAME:.*/$IMAGE_NAME:$IMAGE_TAG/" manifests/${IMAGE_NAME}service.yaml' +# - kubectl version +# - kubectl get all +# - kubectl apply -f "manifests/${IMAGE_NAME}service.yaml" +# - kubectl get all +# # environment: +# # name: test +# # url: https://example.com +# # kubernetes: +# # namespace: test +# rules: +# - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' +# when: manual +# - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' +# when: manual diff --git a/src/kpi_value_api/Dockerfile b/src/kpi_value_api/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..7dd8d307b8338c4a29e97c742ca12a49c4611e0a --- /dev/null +++ b/src/kpi_value_api/Dockerfile @@ -0,0 +1,68 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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. + +FROM python:3.9-slim + +# Install dependencies +RUN apt-get --yes --quiet --quiet update && \ + apt-get --yes --quiet --quiet install wget g++ git && \ + rm -rf /var/lib/apt/lists/* + +# Set Python to show logs as they occur +ENV PYTHONUNBUFFERED=0 + +# Download the gRPC health probe +RUN GRPC_HEALTH_PROBE_VERSION=v0.2.0 && \ + wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ + chmod +x /bin/grpc_health_probe + +# Get generic Python packages +RUN python3 -m pip install --upgrade pip +RUN python3 -m pip install --upgrade setuptools wheel +RUN python3 -m pip install --upgrade pip-tools + +# Get common Python packages +# Note: this step enables sharing the previous Docker build steps among all the Python components +WORKDIR /var/teraflow +COPY common_requirements.in common_requirements.in +RUN pip-compile --quiet --output-file=common_requirements.txt common_requirements.in +RUN python3 -m pip install -r common_requirements.txt + +# Add common files into working directory +WORKDIR /var/teraflow/common +COPY src/common/. ./ +RUN rm -rf proto + +# Create proto sub-folder, copy .proto files, and generate Python code +RUN mkdir -p /var/teraflow/common/proto +WORKDIR /var/teraflow/common/proto +RUN touch __init__.py +COPY proto/*.proto ./ +RUN python3 -m grpc_tools.protoc -I=. --python_out=. --grpc_python_out=. *.proto +RUN rm *.proto +RUN find . -type f -exec sed -i -E 's/(import\ .*)_pb2/from . \1_pb2/g' {} \; + +# Create component sub-folders, get specific Python packages +RUN mkdir -p /var/teraflow/kpi_value_api +WORKDIR /var/teraflow/kpi_value_api +COPY src/kpi_value_api/requirements.in requirements.in +RUN pip-compile --quiet --output-file=requirements.txt requirements.in +RUN python3 -m pip install -r requirements.txt + +# Add component files into working directory +WORKDIR /var/teraflow +COPY src/kpi_value_api/. kpi_value_api/ + +# Start the service +ENTRYPOINT ["python", "-m", "kpi_value_api.service"] diff --git a/src/kpi_value_writer/.gitlab-ci.yml b/src/kpi_value_writer/.gitlab-ci.yml index ffd4e38ffabd0d18b85a86bca7760001a5466b73..52b1b8fe6fd41c5fd8b43adf4ca8da464c8c08ba 100644 --- a/src/kpi_value_writer/.gitlab-ci.yml +++ b/src/kpi_value_writer/.gitlab-ci.yml @@ -13,9 +13,9 @@ # limitations under the License. # Build, tag, and push the Docker image to the GitLab Docker registry -build kpi_manager: +build kpi-value-writer: variables: - IMAGE_NAME: 'kpi_manager' # name of the microservice + IMAGE_NAME: 'kpi-value-writer' # name of the microservice IMAGE_TAG: 'latest' # tag of the container image (production, development, etc) stage: build before_script: @@ -39,45 +39,23 @@ build kpi_manager: - .gitlab-ci.yml # Apply unit test to the component -unit_test context: +unit_test kpi-value-writer: variables: - IMAGE_NAME: 'kpi_manager' # name of the microservice + IMAGE_NAME: 'kpi-value-writer' # name of the microservice IMAGE_TAG: 'latest' # tag of the container image (production, development, etc) stage: unit_test needs: - - build context + - build kpi-value-writer before_script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - if docker network list | grep teraflowbridge; then echo "teraflowbridge is already created"; else docker network create -d bridge teraflowbridge; fi - - if docker container ls | grep crdb; then docker rm -f crdb; else echo "CockroachDB container is not in the system"; fi - - if docker volume ls | grep crdb; then docker volume rm -f crdb; else echo "CockroachDB volume is not in the system"; fi - if docker container ls | grep $IMAGE_NAME; then docker rm -f $IMAGE_NAME; else echo "$IMAGE_NAME container is not in the system"; fi - docker container prune -f script: - docker pull "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG" - - docker pull "cockroachdb/cockroach:latest-v22.2" - - docker volume create crdb - - > - docker run --name crdb -d --network=teraflowbridge -p 26257:26257 -p 8080:8080 - --env COCKROACH_DATABASE=tfs_test --env COCKROACH_USER=tfs --env COCKROACH_PASSWORD=tfs123 - --volume "crdb:/cockroach/cockroach-data" - cockroachdb/cockroach:latest-v22.2 start-single-node - - echo "Waiting for initialization..." - - while ! docker logs crdb 2>&1 | grep -q 'finished creating default user \"tfs\"'; do sleep 1; done - - docker logs crdb - - docker ps -a - - CRDB_ADDRESS=$(docker inspect crdb --format "{{.NetworkSettings.Networks.teraflowbridge.IPAddress}}") - - echo $CRDB_ADDRESS - - NATS_ADDRESS=$(docker inspect nats --format "{{.NetworkSettings.Networks.teraflowbridge.IPAddress}}") - - echo $NATS_ADDRESS - - > - docker run --name $IMAGE_NAME -d -p 1010:1010 - --env "CRDB_URI=cockroachdb://tfs:tfs123@${CRDB_ADDRESS}:26257/tfs_test?sslmode=require" - --volume "$PWD/src/$IMAGE_NAME/tests:/opt/results" - --network=teraflowbridge - $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG - - docker ps -a + - docker run --name $IMAGE_NAME -d -p 30030:30030 -v "$PWD/src/$IMAGE_NAME/tests:/opt/results" --network=teraflowbridge $CI_REGISTRY_IMAGE/$IMAGE_NAME:$IMAGE_TAG - sleep 5 + - docker ps -a - docker logs $IMAGE_NAME - > docker exec -i $IMAGE_NAME bash -c @@ -85,10 +63,8 @@ unit_test context: - docker exec -i $IMAGE_NAME bash -c "coverage report --include='${IMAGE_NAME}/*' --show-missing" coverage: '/TOTAL\s+\d+\s+\d+\s+(\d+%)/' after_script: - - docker volume rm -f crdb + - docker rm -f $IMAGE_NAME - docker network rm teraflowbridge - - docker volume prune --force - - docker image prune --force rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH)' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "develop"' diff --git a/src/kpi_value_writer/Dockerfile b/src/kpi_value_writer/Dockerfile index a57957759a32b45b715e327b54ebe004a6edf265..c84613cfc33278be1b411899e2f85d02e3fbd97b 100644 --- a/src/kpi_value_writer/Dockerfile +++ b/src/kpi_value_writer/Dockerfile @@ -54,15 +54,15 @@ RUN rm *.proto RUN find . -type f -exec sed -i -E 's/(import\ .*)_pb2/from . \1_pb2/g' {} \; # Create component sub-folders, get specific Python packages -RUN mkdir -p /var/teraflow/kpi_manager -WORKDIR /var/teraflow/kpi_manager -COPY src/kpi_manager/requirements.in requirements.in +RUN mkdir -p /var/teraflow/kpi_value_writer +WORKDIR /var/teraflow/kpi_value_writer +COPY src/kpi_value_writer/requirements.in requirements.in RUN pip-compile --quiet --output-file=requirements.txt requirements.in RUN python3 -m pip install -r requirements.txt # Add component files into working directory WORKDIR /var/teraflow -COPY src/kpi_manager/. kpi_manager/ +COPY src/kpi_value_writer/. kpi_value_writer/ # Start the service -ENTRYPOINT ["python", "-m", "kpi_manager.service"] +ENTRYPOINT ["python", "-m", "kpi_value_writer.service"]