diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..25f5a30d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM quay.io/ortelius/ms-python-base:fastapi-1.0 as base + +ENV DB_HOST localhost +ENV DB_NAME postgres +ENV DB_USER postgres +ENV DB_PASS postgres +ENV DB_POST 5432 + +WORKDIR /app + +COPY main.py /app +COPY requirements.txt /app +RUN pip install -r requirements.txt; \ +python -m pip uninstall -y pip; diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/__pycache__/main.cpython-38.pyc b/__pycache__/main.cpython-38.pyc deleted file mode 100644 index ad91191d..00000000 Binary files a/__pycache__/main.cpython-38.pyc and /dev/null differ diff --git a/chart/ms-validate-user/.helmignore b/chart/ms-validate-user/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/chart/ms-validate-user/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/chart/ms-validate-user/Chart.yaml b/chart/ms-validate-user/Chart.yaml new file mode 100644 index 00000000..79419600 --- /dev/null +++ b/chart/ms-validate-user/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: ms-compitem-crud +description: Dependency Packages +type: application +version: 0.1.0 +appVersion: "1.0" diff --git a/chart/ms-validate-user/templates/_helpers.tpl b/chart/ms-validate-user/templates/_helpers.tpl new file mode 100644 index 00000000..2c9e5706 --- /dev/null +++ b/chart/ms-validate-user/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ms-compitem-crud.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ms-compitem-crud.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ms-compitem-crud.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + diff --git a/chart/ms-validate-user/templates/deployment.yaml b/chart/ms-validate-user/templates/deployment.yaml new file mode 100644 index 00000000..d6edc710 --- /dev/null +++ b/chart/ms-validate-user/templates/deployment.yaml @@ -0,0 +1,52 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ms-compitem-crud.name" . }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "ms-compitem-crud.name" . }} + tier: backend + track: stable + template: + metadata: + labels: + app: {{ include "ms-compitem-crud.name" . }} + tier: backend + track: stable + spec: + containers: + - name: {{ include "ms-compitem-crud.name" . }} + image: "{{ .Values.DockerRepo }}:{{ .Values.DockerTag }}" + imagePullPolicy: Always + env: + - name: DB_USER + valueFrom: + secretKeyRef: + name: pgcred + key: DBUserName + - name: DB_PASS + valueFrom: + secretKeyRef: + name: pgcred + key: DBPassword + - name: DB_HOST + valueFrom: + secretKeyRef: + name: pgcred + key: DBHost + - name: DB_PORT + valueFrom: + secretKeyRef: + name: pgcred + key: DBPort + - name: DB_NAME + valueFrom: + secretKeyRef: + name: pgcred + key: DBName + ports: + - name: http + containerPort: 80 +--- diff --git a/chart/ms-validate-user/templates/service.yaml b/chart/ms-validate-user/templates/service.yaml new file mode 100644 index 00000000..9b45f57c --- /dev/null +++ b/chart/ms-validate-user/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ms-compitem-crud.name" . }} +spec: + selector: + app: {{ include "ms-compitem-crud.name" . }} + ports: + - protocol: TCP + port: 80 + targetPort: 80 + type: NodePort +--- diff --git a/chart/ms-validate-user/values.yaml b/chart/ms-validate-user/values.yaml new file mode 100644 index 00000000..57a35f4f --- /dev/null +++ b/chart/ms-validate-user/values.yaml @@ -0,0 +1,31 @@ +nameOverride: "" +fullnameOverride: "" + +replicaCount: 1 + +image: + # TODO - update image params when image is pushed to repo. + repository: quay.io/ortelius/ms-compitem-crud + #digest: + tag: main-v9.0.0.17-g106d15a + +service: + type: NodePort + portName: env2app-port + exposedPort: 8080 + targetPort: 80 + nodePort: 30080 + +envVars: + DB_HOST: 192.168.10.96 + DB_PORT: 6543 + DB_NAME: postgres + + +resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi diff --git a/cloudbuild/cloudbuild.yaml b/cloudbuild/cloudbuild.yaml new file mode 100644 index 00000000..073cc789 --- /dev/null +++ b/cloudbuild/cloudbuild.yaml @@ -0,0 +1,84 @@ +steps: + # Get ssh key from Google Secret Manager + - name: gcr.io/cloud-builders/gcloud + id: ssh_keys + entrypoint: 'bash' + args: [ '-c', 'gcloud secrets versions access latest --secret=github > /root/.ssh/id_rsa;chmod 600 /root/.ssh/id_rsa;ssh-keyscan -t rsa github.com > /root/.ssh/known_hosts' ] + volumes: + - name: 'ssh' + path: /root/.ssh + + # Login to Quay for push. + - name: 'gcr.io/cloud-builders/docker' + id: login + waitFor: ['ssh_keys'] + entrypoint: 'bash' + args: ['-c', 'docker login quay.io --username "$$QUAY_USERID" --password $$QUAY_PASSWORD'] + secretEnv: ['QUAY_USERID', 'QUAY_PASSWORD'] + env: + - 'DOCKER_CONFIG=/workspace/docker-config' + + # Setup environment including img tag name for nginx + - name: gcr.io/cloud-builders/docker + id: env + waitFor: ['login'] + entrypoint: 'bash' + args: ['-c', 'ls -A1 | grep -v docker-config | xargs rm -rf;git init; git remote add origin $$COMPONENT_GITURL;git fetch;git checkout --track -b $BRANCH_NAME origin/$BRANCH_NAME;env | sed "s/^/export /" >> /workspace/cloudbuild.sh'] + volumes: + - name: 'ssh' + path: /root/.ssh + env: + - 'COMPONENT_APPLICATION=GLOBAL.ortelius.saas.ortelius-devops' + - 'COMPONENT_NAME=GLOBAL.ortelius.saas.ms-compitem-crud' + - 'COMPONENT_GITURL=git@github.com:ortelius/ortelius-ms-compitem-crud.git' + - 'COMPONENT_VARIANT=$BRANCH_NAME' + - 'COMPONENT_VERSION=10.0.0' + - 'COMPONENT_VERSION_COMMIT="v$$COMPONENT_VERSION.$$(git rev-list --count $BRANCH_NAME)-g$SHORT_SHA"' + - 'COMPONENT_DOCKERREPO=quay.io/ortelius/ms-compitem-crud' + - 'COMPONENT_CUSTOMACTION=GLOBAL.HelmChart' + - 'COMPONENT_CHARTNAME=chart/ms-compitem-crud' + - 'COMPONENT_CHARTNAMESPACE=ortelius' + - 'DEPLOY_ENV=GLOBAL.ortelius.saas.aks-cluster' + - 'BLDDATE=`date`' + - 'IMAGE_TAG="$BRANCH_NAME-v$$COMPONENT_VERSION.$$(git rev-list --count $BRANCH_NAME)-g$SHORT_SHA"' + - 'DOCKER_CONFIG=/workspace/docker-config' + + # Build and push quay.io/ortelius/ms-compitem-crud + - name: 'gcr.io/cloud-builders/docker' + id: build_push + waitFor: ['env'] + entrypoint: 'bash' + args: ["-c", '. /workspace/cloudbuild.sh;docker build --tag $$COMPONENT_DOCKERREPO:$$IMAGE_TAG -f /workspace/Dockerfile .;docker push $$COMPONENT_DOCKERREPO:$$IMAGE_TAG'] + env: + - 'DOCKER_CONFIG=/workspace/docker-config' + + # Get image id + - name: 'gcr.io/cloud-builders/docker' + id: digest + waitFor: ['build_push'] + entrypoint: 'bash' + env: + - 'DOCKER_CONFIG=/workspace/docker-config' + args: ['-c', ". /workspace/cloudbuild.sh;echo export DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' $$COMPONENT_DOCKERREPO:$$IMAGE_TAG) >> /workspace/cloudbuild.sh" ] + + # Capture new component version in DeployHub + - name: 'quay.io/deployhub/compupdate' + id: compupdate + waitFor: ['digest'] + entrypoint: 'bash' + secretEnv: ['DHUSER', 'DHPASS'] + args: ['-c', '. /workspace/cloudbuild.sh;dh updatecomp --dhurl https://console.deployhub.com --appname "$$COMPONENT_APPLICATION" --compname "$$COMPONENT_NAME" --compvariant "$$COMPONENT_VARIANT" --compversion "$$COMPONENT_VERSION_COMMIT" --deployenv "$$DEPLOY_ENV" --docker --compattr "GitCommit:$SHORT_SHA" --compattr "GitUrl:$$COMPONENT_GITURL" --compattr "GitRepo:ortelius/$REPO_NAME" --compattr "GitTag:$TAG_NAME" --compattr "GitBranch:$BRANCH_NAME" --compattr "Chart:$$COMPONENT_CHARTNAME" --compattr "DockerSha:$$DIGEST" --compattr "DockerBuildDate:$$BLDDATE" --compattr "DockerRepo:$$COMPONENT_DOCKERREPO" --compattr "BuildId:$BUILD_ID" --compattr "BuildUrl:https://console.cloud.google.com/cloud-build/builds/$BUILD_ID?project=$PROJECT_ID" --compattr "CustomAction:$$COMPONENT_CUSTOMACTION" --compattr "DockerTag:$$IMAGE_TAG" --compattr "ChartNamespace:$$COMPONENT_CHARTNAMESPACE"'] + +secrets: +- kmsKeyName: projects/eighth-physics-169321/locations/global/keyRings/cli/cryptoKeys/quay + secretEnv: + QUAY_USERID: CiQAW+P1J9UZz+Hr1uonladAW2dKqaiVd5ux8Q9EV81pK0u5V+4SNACcBdnKacvH4QXPamH1N4uJZvZ/0TMwvELgXAAlP0wR2zBw2WhCV82GMiUkW3iGVlbqz7c= +- kmsKeyName: projects/eighth-physics-169321/locations/global/keyRings/cli/cryptoKeys/quay-pw + secretEnv: + QUAY_PASSWORD: CiQAUULEud9Ej8XtwNAb9gkbDVhSGFZYhUGE30fNwR+7ehAOkH8SMgCz6KYeykjgS16RPxgKlrIQL/1TKDt06v4OXGIisFXOkdWC+jvdda8mTzVNCi8sT5g6 +- kmsKeyName: projects/eighth-physics-169321/locations/global/keyRings/cli/cryptoKeys/ortelius-id + secretEnv: + DHUSER: CiQAGgJuQMHWANazqTOeE/SyoX/YNVWnES7eJEVWY8mTP98Er3USMQC43iiopoGYhP/YahsQu/yUURiqJBVZURYiUiu5Z7UBkrDgUAonKCKjtzeSNUP7HoQ= +- kmsKeyName: projects/eighth-physics-169321/locations/global/keyRings/cli/cryptoKeys/ortelius-pw + secretEnv: + DHPASS: CiQAZySXz07McN9e6fyr6X4qwkw4iBgeULmpq16RbxIAcqg6gTESMQB98+y30zqMVPx2S/Q/8ld+qlJWWxmocnbjLe9iyepMwyMl3yf+r5e55nf85PlrBBw= diff --git a/main.py b/main.py index 9deacf38..48a01976 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,26 @@ -import json import os +from collections import OrderedDict +import uvicorn import psycopg2 -import pybreaker import psycopg2.extras import requests -from flask import Flask, request -from flask_restful import Api, Resource -from collections import OrderedDict +from sqlalchemy import create_engine +from fastapi import FastAPI, Request, Response, HTTPException, status +from pydantic import BaseModel +from typing import List, Optional +from sqlalchemy.exc import OperationalError, StatementError +from time import sleep +import logging + +# Init Globals +service_name = 'ortelius-ms-compitem-crud' +db_conn_retry = 3 -# Init Flask -app = Flask(__name__) -api = Api(app) +app = FastAPI( + title=service_name, + description=service_name + ) # Init db connection db_host = os.getenv("DB_HOST", "localhost") @@ -21,179 +30,438 @@ db_port = os.getenv("DB_PORT", "5432") validateuser_url = os.getenv("VALIDATEUSER_URL", "http://localhost:5000") -conn_circuit_breaker = pybreaker.CircuitBreaker( - fail_max=1, - reset_timeout=10, -) +engine = create_engine("postgresql+psycopg2://" + db_user + ":" + db_pass + "@" + db_host +":"+ db_port + "/" + db_name, pool_pre_ping=True) -@conn_circuit_breaker -def create_conn(): - conn = psycopg2.connect(host=db_host, database=db_name, user=db_user, password=db_pass, port=db_port) - return conn - -class CompItem(Resource): +# health check endpoint +class StatusMsg(BaseModel): + status: str + service_name: Optional[str] = None + +@app.get("/health", + responses={ + 503: {"model": StatusMsg, + "description": "DOWN Status for the Service", + "content": { + "application/json": { + "example": {"status": 'DOWN'} + }, + }, + }, + 200: {"model": StatusMsg, + "description": "UP Status for the Service", + "content": { + "application/json": { + "example": {"status": 'UP', "service_name": service_name} + } + }, + }, + } + ) +async def health(response: Response): + try: + with engine.connect() as connection: + conn = connection.connection + cursor = conn.cursor() + cursor.execute('SELECT 1') + if cursor.rowcount > 0: + return {"status": 'UP', "service_name": service_name} + response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE + return {"status": 'DOWN'} - @classmethod - def get(cls): + except Exception as err: + print(str(err)) + response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE + return {"status": 'DOWN'} +# end health check +class CompItemModel(BaseModel): + compid: int + id: int + repositoryid: Optional[int] = None + target: Optional[str] = None + name: Optional[str] = None + summary: Optional[str] = None + predecessorid: Optional[int] = None + xpos: Optional[int] = None + ypos: Optional[int] = None + creatorid: Optional[int] = None + created: Optional[int] = None + modifierid: Optional[int] = None + modified: Optional[int] = None + status: Optional[str] = None + rollup: Optional[int] = None + rollback: Optional[int] = None + kind: Optional[str] = None + buildid: Optional[str] = None + buildurl: Optional[str] = None + chart: Optional[str] = None + operator: Optional[str] = None + builddate: Optional[str] = None + dockersha: Optional[str] = None + gitcommit: Optional[str] = None + gitrepo: Optional[str] = None + gittag: Optional[str] = None + giturl: Optional[str] = None + dockerrepo: Optional[str] = None + chartversion: Optional[str] = None + chartnamespace: Optional[str] = None + dockertag: Optional[str] = None + chartrepo: Optional[str] = None + chartrepourl: Optional[str] = None + serviceowner: Optional[str] = None + serviceowneremail: Optional[str] = None + serviceownerphone: Optional[str] = None + slackchannel: Optional[str] = None + discordchannel: Optional[str] = None + hipchatchannel: Optional[str] = None + pagerdutyurl: Optional[str] = None + pagerdutybusinessurl: Optional[str] = None + +class CompItemModelList(BaseModel): + data: List[CompItemModel] + +class Message(BaseModel): + detail: str + + +@app.get('/msapi/compitem', + response_model=List[CompItemModel], + responses={ + 401: {"model": Message, + "description": "Authorization Status", + "content": { + "application/json": { + "example": {"detail": "Authorization failed"} + }, + }, + }, + 500: {"model": Message, + "description": "SQL Error", + "content": { + "application/json": { + "example": {"detail": "SQL Error: 30x"} + }, + }, + } #, + # 200: { + # "description": "List of domain ids the user belongs to.", + # "content": { + # "application/json": { + # "example": [1, 200, 201, 5033] + # } + # }, + # }, + } + ) +async def get_compitem(request: Request, compitemid:int): + try: result = requests.get(validateuser_url + "/msapi/validateuser", cookies=request.cookies) if (result is None): - return None, 404 + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed") + + if (result.status_code != status.HTTP_200_OK): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed status_code=" + str(result.status_code)) + except Exception as err: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed:" + str(err)) from None - if (result.status_code != 200): - return result.json(), 404 - - try: - compitemid = request.args.get('compitemid',"-1") - conn = create_conn() - cursor = conn.cursor(cursor_factory = psycopg2.extras.RealDictCursor) - sql = """select compid, id, name, rollup, rollback, repositoryid, target, xpos, ypos, - kind, buildid, buildurl, chart, operator, builddate, dockersha, gitcommit, - gitrepo, gittag, giturl, chartversion, chartnamespace, dockertag, chartrepo, - chartrepourl, serviceowner, serviceowneremail, serviceownerphone, - slackchannel, discordchannel, hipchatchannel, pagerdutyurl, pagerdutybusinessurl - from dm.dm_componentitem where id = %s""" - - params = (compitemid,) - cursor.execute(sql, params) - result = cursor.fetchall() - if (not result): - result = [OrderedDict([('compid', -1), ('id', compitemid), ('name', None), ('rollup', None), ('rollback', None), ('repositoryid', None), - ('target', None), ('xpos', None), ('ypos', None), ('kind', None), ('buildid', None), ('buildurl', None), - ('chart', None), ('operator', None), ('builddate', None), ('dockersha', None), ('gitcommit', None), - ('gitrepo', None), ('gittag', None), ('giturl', None), ('chartversion', None), ('chartnamespace', None), ('dockertag', None), ('chartrepo', None), - ('chartrepourl', None), ('serviceowner', None), ('serviceowneremail', None), ('serviceownerphone', None), - ('slackchannel', None), ('discordchannel', None), ('hipchatchannel', None), ('pagerdutyurl', None), ('pagerdutybusinessurl', None)])] - - print(result) - return result - except Exception as err: - print(err) - return err - - @classmethod - def post(cls): # completed - try: + try: + #Retry logic for failed query + no_of_retry = db_conn_retry + attempt = 1; + while True: + try: + with engine.connect() as connection: + conn = connection.connection + authorized = False # init to not authorized + cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + sql = """select compid, id, name, rollup, rollback, repositoryid, target, xpos, ypos, + kind, buildid, buildurl, chart, operator, builddate, dockersha, gitcommit, + gitrepo, gittag, giturl, chartversion, chartnamespace, dockertag, chartrepo, + chartrepourl, serviceowner, serviceowneremail, serviceownerphone, + slackchannel, discordchannel, hipchatchannel, pagerdutyurl, pagerdutybusinessurl + from dm.dm_componentitem where id = %s""" - result = requests.get(validateuser_url + "/msapi/validateuser", cookies=request.cookies) + params = (str(compitemid),) + cursor.execute(sql, params) + result = cursor.fetchall() + if (not result): + result = [OrderedDict([('compid', -1), ('id', compitemid), ('name', None), ('rollup', None), ('rollback', None), ('repositoryid', None), + ('target', None), ('xpos', None), ('ypos', None), ('kind', None), ('buildid', None), ('buildurl', None), + ('chart', None), ('operator', None), ('builddate', None), ('dockersha', None), ('gitcommit', None), + ('gitrepo', None), ('gittag', None), ('giturl', None), ('chartversion', None), ('chartnamespace', None), ('dockertag', None), ('chartrepo', None), + ('chartrepourl', None), ('serviceowner', None), ('serviceowneremail', None), ('serviceownerphone', None), + ('slackchannel', None), ('discordchannel', None), ('hipchatchannel', None), ('pagerdutyurl', None), ('pagerdutybusinessurl', None)])] + cursor.close() + conn.close() + return result - if (result is None): - return None, 404 + except (InterfaceError, OperationalError) as ex: + if attempt < no_of_retry: + logging.error( + "Database connection error: {} - sleeping for {}s" + " and will retry (attempt #{} of {})".format( + ex, sleep_for, attempt, no_of_retry + ) + ) + #200ms of sleep time in cons. retry calls + sleep(0.2) + attempt += 1 + continue + else: + raise + + except HTTPException: + raise + except Exception as err: + print(err) + # conn.rollback() + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(err)) from None + +# Not implemented fully. SQL query is not complete + +@app.post("/msapi/compitem", + responses={ + 401: {"model": Message, + "description": "Authorization Status", + "content": { + "application/json": { + "example": {"detail": "Authorization failed"} + }, + }, + }, + 500: {"model": Message, + "description": "SQL Error", + "content": { + "application/json": { + "example": {"detail": "SQL Error: 30x"} + }, + }, + } + } + ) +async def create_compitem(response: Response, request: Request, compItemList: List[CompItemModel]): - if (result.status_code != 200): - return result.json(), 404 + try: + result = requests.get(validateuser_url + "/msapi/validateuser", cookies=request.cookies) + if (result is None): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed") + + if (result.status_code != status.HTTP_200_OK): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed status_code=" + str(result.status_code)) + except Exception as err: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed:" + str(err)) from None + + try: + data_list = [] + for col in compItemList: + row = (col.id, col.compid, col.status, col.buildid, col.buildurl, col.dockersha, col.dockertag, col.gitcommit, col.gitrepo, col.giturl) # this will be changed + data_list.append(row) - input = request.get_json(); - data_list = [] - for i in input: - d = (i['id'], i['compid'], i['status'], i['buildid'], i['buildurl'], i['dockersha'], i['dockertag'], i['gitcommit'], i['gitrepo'], i['giturl']) # this will be changed - data_list.append(d) - - print (data_list) - conn = create_conn() - cursor = conn.cursor() - # execute the INSERT statement - records_list_template = ','.join(['%s'] * len(data_list)) - sql = 'INSERT INTO dm.dm_componentitem(id, compid, status, buildid, buildurl, dockersha, dockertag, gitcommit, gitrepo, giturl) \ - VALUES {}'.format(records_list_template) - cursor.execute(sql, data_list) - # commit the changes to the database - rows_inserted = cursor.rowcount - # Commit the changes to the database - conn.commit() - return rows_inserted - - except Exception as err: - print(err) - conn = create_conn() - cursor = conn.cursor() - cursor.execute("ROLLBACK") - conn.commit() - return err - - @classmethod - def delete(cls): - try: - result = requests.get(validateuser_url + "/msapi/validateuser", cookies=request.cookies) - if (result is None): - return None, 404 + records_list_template = ','.join(['%s'] * len(data_list)) + sql = 'INSERT INTO dm.dm_componentitem(id, compid, status, buildid, buildurl, dockersha, dockertag, gitcommit, gitrepo, giturl) \ + VALUES {}'.format(records_list_template) + + #Retry logic for failed query + no_of_retry = db_conn_retry + attempt = 1; + while True: + try: + with engine.connect() as connection: + conn = connection.connection + cursor = conn.cursor() + cursor.execute(sql, data_list) + # commit the changes to the database + rows_inserted = cursor.rowcount + # Commit the changes to the database + conn.commit() + conn.close() + + if rows_inserted > 0: + response.status_code = status.HTTP_201_CREATED + return {"message": 'components created succesfully'} + + response.status_code = status.HTTP_200_OK + return {"message": 'components not created'} + + except (InterfaceError, OperationalError) as ex: + if attempt < no_of_retry: + logging.error( + "Database connection error: {} - sleeping for {}s" + " and will retry (attempt #{} of {})".format( + ex, sleep_for, attempt, no_of_retry + ) + ) + #200ms of sleep time in cons. retry calls + sleep(0.2) + attempt += 1 + continue + else: + raise + + except HTTPException: + raise + except Exception as err: + print(err) + # conn.rollback() + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(err)) from None + +@app.delete("/msapi/compitem", + responses={ + 401: {"model": Message, + "description": "Authorization Status", + "content": { + "application/json": { + "example": {"detail": "Authorization failed"} + }, + }, + }, + 500: {"model": Message, + "description": "SQL Error", + "content": { + "application/json": { + "example": {"detail": "SQL Error: 30x"} + }, + }, + } + } + ) +async def delete_compitem(request: Request, compid: int): + + try: + result = requests.get(validateuser_url + "/msapi/validateuser", cookies=request.cookies) + if (result is None): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed") - if (result.status_code != 200): - return result.json(), 404 + if (result.status_code != status.HTTP_200_OK): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed status_code=" + str(result.status_code)) + except Exception as err: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed:" + str(err)) from None + + try: - comp_id = request.args.get('comp_id') - #comp_item_id = request.args.get('comp_item_id') - conn = create_conn() - cursor = conn.cursor() - sql = "select id from dm.dm_componentitem where compid = " + comp_id - t = tuple() - l = [] - cursor.execute(sql) - row = cursor.fetchone() - while row: - print (row) - l = list(t) - l.append(row[0]) - t = tuple(l) - row = cursor.fetchone() - - sql1 = "DELETE from dm.dm_compitemprops where compitemid in " + str(t) - sql2 = "DELETE from dm.dm_componentitem where compid=" + comp_id - rows_deleted = 0 - with conn.cursor() as cursor: - cursor.execute(sql1) - cursor.execute(sql2) - rows_deleted = cursor.rowcount - # Commit the changes to the database - conn.commit() - return rows_deleted - except Exception as err: - print(err) - return err - - @classmethod - def put(cls): # not completed - try: + #Retry logic for failed query + no_of_retry = db_conn_retry + attempt = 1; + while True: + try: + with engine.connect() as connection: + conn = connection.connection + cursor = conn.cursor() + + sql1 = "DELETE from dm.dm_compitemprops where compitemid in (select id from dm.dm_componentitem where compid = " + str(compid) + ")" + sql2 = "DELETE from dm.dm_componentitem where compid=" + str(compid) + rows_deleted = 0 + cursor.execute(sql1) + cursor.execute(sql2) + rows_deleted = cursor.rowcount + # Commit the changes to the database + conn.commit() - result = requests.get(validateuser_url + "/msapi/validateuser", cookies=request.cookies) - if (result is None): - return None, 404 + # response.status_code = status.HTTP_200_OK + return {"message": 'component deleted succesfully'} + + except (InterfaceError, OperationalError) as ex: + if attempt < no_of_retry: + logging.error( + "Database connection error: {} - sleeping for {}s" + " and will retry (attempt #{} of {})".format( + ex, sleep_for, attempt, no_of_retry + ) + ) + #200ms of sleep time in cons. retry calls + sleep(0.2) + attempt += 1 + continue + else: + raise + + except HTTPException: + raise + except Exception as err: + print(err) + # conn.rollback() + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(err)) from None + +@app.put("/msapi/compitem", + responses={ + 401: {"model": Message, + "description": "Authorization Status", + "content": { + "application/json": { + "example": {"detail": "Authorization failed"} + }, + }, + }, + 500: {"model": Message, + "description": "SQL Error", + "content": { + "application/json": { + "example": {"detail": "SQL Error: 30x"} + }, + }, + } + } + ) +async def update_compitem(request: Request, compitemList: List[CompItemModel]): + + try: + result = requests.get(validateuser_url + "/msapi/validateuser", cookies=request.cookies) + if (result is None): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed") - if (result.status_code != 200): - return result.json(), 404 + if (result.status_code != status.HTTP_200_OK): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed status_code=" + str(result.status_code)) + except Exception as err: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authorization Failed:" + str(err)) from None + + try: - input = request.get_json(); - data_list = [] - # for i in input: - # d = (i['id'], i['compid'], i['status'], i['buildid'], i['buildurl'], i['dockersha'], i['dockertag'], i['gitcommit'], i['gitrepo'], i['giturl']) # this will be changed - # data_list.append(d) - - # print (data_list) - conn = create_conn() - cursor = conn.cursor() - # # execute the INSERT statement - # records_list_template = ','.join(['%s'] * len(data_list)) - # sql = 'INSERT INTO dm.dm_componentitem(id, compid, status, buildid, buildurl, dockersha, dockertag, gitcommit, gitrepo, giturl) \ - # VALUES {}'.format(records_list_template) - cursor.execute(sql, data_list) - # commit the changes to the database - rows_inserted = cursor.rowcount - # Commit the changes to the database - conn.commit() - return rows_inserted - - except Exception as err: - print(err) - conn = create_conn() - cursor = conn.cursor() - cursor.execute("ROLLBACK") - conn.commit() - return err + data_list = [] + for col in compitemList: + row = (col.compid, col.status, col.buildid, col.buildurl, col.dockersha, col.dockertag, col.gitcommit, col.gitrepo, col.giturl, col.id) # this will be changed + data_list.append(row) + + #Retry logic for failed query + no_of_retry = db_conn_retry + attempt = 1; + while True: + try: + with engine.connect() as connection: + conn = connection.connection + cursor = conn.cursor() + # # execute the INSERT statement + # records_list_template = ','.join(['%s'] * len(data_list)) + sql = 'UPDATE dm.dm_componentitem set compid=%s, status=%s, buildid=%s, buildurl=%s, dockersha=%s, dockertag=%s, gitcommit=%s, gitrepo=%s, giturl=%s \ + WHERE id = %s' + cursor.executemany(sql, data_list) + # commit the changes to the database + rows_inserted = cursor.rowcount + # Commit the changes to the database + conn.commit() + + if rows_inserted > 0: + return {"message": 'components updated succesfully'} -## -# Actually setup the Api resource routing here -## -api.add_resource(CompItem, '/msapi/compitem') - -if __name__ == '__main__': - app.run(host="0.0.0.0", port=5001) + return {"message": 'components not updated'} + + except (InterfaceError, OperationalError) as ex: + if attempt < no_of_retry: + logging.error( + "Database connection error: {} - sleeping for {}s" + " and will retry (attempt #{} of {})".format( + ex, sleep_for, attempt, no_of_retry + ) + ) + #200ms of sleep time in cons. retry calls + sleep(0.2) + attempt += 1 + continue + else: + raise + + except Exception as err: + print(err) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(err)) from None + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/requirements.txt b/requirements.txt index 8bad8efc..b4d72822 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,6 @@ -aniso8601==9.0.1 -click==7.1.2 -Flask==1.1.2 -Flask-RESTful==0.3.8 -itsdangerous==1.1.0 -Jinja2==2.11.3 -MarkupSafe==1.1.1 -psycopg2-binary==2.8.6 -pytz==2021.1 -six==1.15.0 -Werkzeug==1.0.1 +# psycopg2==2.8.6 - installed by base image +#sqlalchemy - installed by base image +#fastapi - installed by base image +uvicorn==0.34.0 +pydantic==2.11.3 +requests==2.32.3 \ No newline at end of file diff --git a/workspace.code-workspace b/workspace.code-workspace new file mode 100644 index 00000000..876a1499 --- /dev/null +++ b/workspace.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file