commit ad7f7acdcaaebd8ef36b12e6c8986aabefc55602 Author: Lauri Võsandi Date: Mon Aug 1 19:25:56 2022 +0300 Initial commit diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..e7b5dfa --- /dev/null +++ b/.drone.yml @@ -0,0 +1,2 @@ +kind: template +load: docker.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c8a2970 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM harbor.k-space.ee/k-space/microservice-base +ADD camera-operator.py /camera-operator.py +ADD camera-deployment.yml camera-service.yml /config/ +WORKDIR /config +ENTRYPOINT /camera-operator.py diff --git a/camera-deployment.yml b/camera-deployment.yml new file mode 100644 index 0000000..6d5363b --- /dev/null +++ b/camera-deployment.yml @@ -0,0 +1,94 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: foobar + labels: {} + + # Make sure keel.sh pulls updates for this deployment + annotations: + keel.sh/policy: force + keel.sh/trigger: poll +spec: + replicas: 1 + + # Make sure we do not congest the network during rollout + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + selector: + matchLabels: + app: foobar + template: + metadata: + annotations: + prometheus.io/scrape: 'true' + prometheus.io/port: '5000' + labels: + app: foobar + component: camdetect + spec: + containers: + - name: camdetect + image: harbor.k-space.ee/k-space/camera-motion-detect:latest + resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" + securityContext: + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + command: + - /app/camdetect.py + env: + - name: SOURCE_NAME + value: foobar + - name: S3_ENDPOINT_URL + value: http://minio:9000 + - name: MJPEGSTREAMER_CREDENTIALS + valueFrom: + secretKeyRef: + name: application-secrets + key: MJPEGSTREAMER_CREDENTIALS + - name: MONGO_URI + valueFrom: + secretKeyRef: + name: mongodb-application-application + key: connectionString.standard + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: minio-secret + key: accesskey + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: minio-secret + key: secretkey + + # Make sure 2+ pods of same camera are scheduled on different hosts + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: + - foobar + topologyKey: kubernetes.io/hostname + + # Make sure camera deployments are spread over workers + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app: foobar + component: camdetect diff --git a/camera-operator.py b/camera-operator.py new file mode 100644 index 0000000..dd007ed --- /dev/null +++ b/camera-operator.py @@ -0,0 +1,87 @@ +import asyncio +import base64 +import yaml +from kubernetes_asyncio.client.api_client import ApiClient +from kubernetes_asyncio import client, config +from os import path +from time import time + +LABEL_MANAGED_BY = "camera-operator" +with open("camera-service.yml") as stream: + SERVICE_BODY = stream.read() +with open("camera-deployment.yml") as stream: + DEPLOYMENT_BODY = stream.read() + +async def main(): + + await config.load_kube_config() + async with ApiClient() as api: + + v1 = client.CoreV1Api(api) + apps_api = client.AppsV1Api() + + print("Listing namespaces") + ret = await v1.list_namespace() + api_instance = client.CustomObjectsApi(api) + now = str(time()) + + for i in ret.items: + try: + resp = await api_instance.list_namespaced_custom_object( + "k-space.ee", "v1alpha1", i.metadata.name, "cams") + except client.exceptions.ApiException: + continue + for item in resp["items"]: + target = item["spec"]["target"] + secret_ref = item["spec"].get("secretRef") + replicas = item["spec"].get("replicas") + + print("Applying", target) + name = "camera-%s" % item["metadata"]["name"] + + # Generate Deployment + body = yaml.safe_load(DEPLOYMENT_BODY.replace("foobar", name)) + body["metadata"]["labels"] ["app.kubernetes.io/managed-by"] = LABEL_MANAGED_BY + body["metadata"]["labels"] ["modified"] = now + body["spec"]["template"]["spec"]["containers"][0]["args"] = [target] + if replicas: + body["spec"]["replicas"] = replicas + try: + await apps_api.replace_namespaced_deployment( + name = name, body = body, namespace=i.metadata.name) + print("Updated deployment %s/%s" % (i.metadata.name, name)) + except client.exceptions.ApiException as e: + await apps_api.create_namespaced_deployment( + body = body, namespace=i.metadata.name) + print("Created deployment %s/%s" % (i.metadata.name, name)) + + # Generate Service + body = yaml.safe_load(SERVICE_BODY.replace("foobar", name)) + body["metadata"]["labels"] ["app.kubernetes.io/managed-by"] = LABEL_MANAGED_BY + body["metadata"]["labels"] ["modified"] = now + try: + await v1.replace_namespaced_service( + name = name, body = body, namespace=i.metadata.name) + print("Updated service %s/%s" % (i.metadata.name, name)) + except client.exceptions.ApiException as e: + await v1.create_namespaced_service( + body = body, namespace=i.metadata.name) + print("Created service %s/%s" % (i.metadata.name, name)) + + deployments = await apps_api.list_deployment_for_all_namespaces() + for dep in deployments.items: + if not dep.metadata.labels: + continue + if dep.metadata.labels.get("app.kubernetes.io/managed-by") != LABEL_MANAGED_BY: + continue + if dep.metadata.labels.get("modified") == now: + continue + print("Removing deployment: %s/%s" % (dep.metadata.namespace, dep.metadata.name)) + await apps_api.delete_namespaced_deployment(name=dep.metadata.name, namespace=dep.metadata.namespace) + print("Done") + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.close() + diff --git a/camera-service.yml b/camera-service.yml new file mode 100644 index 0000000..49c60e4 --- /dev/null +++ b/camera-service.yml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: foobar + labels: + component: camdetect +spec: + type: ClusterIP + selector: + app: foobar + component: camdetect + ports: + - protocol: TCP + port: 80 + targetPort: 5000 diff --git a/cameras.yml b/cameras.yml new file mode 100644 index 0000000..13864c8 --- /dev/null +++ b/cameras.yml @@ -0,0 +1,67 @@ +--- +apiVersion: k-space.ee/v1alpha1 +kind: Camera +metadata: + name: workshop +spec: + target: http://workshop.cam.k-space.ee:8080/?action=stream + secretRef: camera-secrets +--- +apiVersion: k-space.ee/v1alpha1 +kind: Camera +metadata: + name: server-room +spec: + target: http://server-room.cam.k-space.ee:8080/?action=stream + secretRef: camera-secrets + replicas: 2 +--- +apiVersion: k-space.ee/v1alpha1 +kind: Camera +metadata: + name: printer +spec: + target: http://printer.cam.k-space.ee:8080/?action=stream + secretRef: camera-secrets + replicas: 2 +--- +apiVersion: k-space.ee/v1alpha1 +kind: Camera +metadata: + name: chaos +spec: + target: http://chaos.cam.k-space.ee:8080/?action=stream + secretRef: camera-secrets +--- +apiVersion: k-space.ee/v1alpha1 +kind: Camera +metadata: + name: cyber +spec: + target: http://cyber.cam.k-space.ee:8080/?action=stream + secretRef: camera-secrets +--- +apiVersion: k-space.ee/v1alpha1 +kind: Camera +metadata: + name: kitchen +spec: + target: http://kitchen.cam.k-space.ee:8080/?action=stream + secretRef: camera-secrets +--- +apiVersion: k-space.ee/v1alpha1 +kind: Camera +metadata: + name: back-door +spec: + target: http://back-door.cam.k-space.ee:8080/?action=stream + secretRef: camera-secrets +--- +apiVersion: k-space.ee/v1alpha1 +kind: Camera +metadata: + name: ground-door +spec: + target: http://ground-door.cam.k-space.ee:8080/?action=stream + secretRef: camera-secrets + diff --git a/crd.yaml b/crd.yaml new file mode 100644 index 0000000..363bfd4 --- /dev/null +++ b/crd.yaml @@ -0,0 +1,72 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: cams.k-space.ee +spec: + group: k-space.ee + names: + plural: cams + singular: cam + kind: Camera + shortNames: + - cam + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + roi: + type: object + description: Region of interest for this camera + properties: + threshold: + type: integer + description: Percentage of pixels changed within ROI to + consider whole frame to have motion detected. + Defaults to 5. + enabled: + type: boolean + description: Whether motion detection is enabled for this + camera. Defaults to false. + left: + type: integer + description: Left boundary of ROI as + percentage of the width of a frame. + By default 0. + right: + type: integer + description: Right boundary of ROI as + percentage of the width of a frame. + By default 100. + top: + type: integer + description: Top boundary of ROI as + percentage of the height of a frame + By deafault 0. + bottom: + type: integer + description: Bottom boundary of ROI as + percentage of the height of a frame. + By default 100. + secretRef: + type: string + description: Secret that contains authentication credentials + target: + type: string + description: URL of the video feed stream + replicas: + type: integer + minimum: 1 + maximum: 2 + description: For highly available deployment set this to 2 or + higher. Make sure you also run Mongo and Minio in HA + configurations + required: ["target"] + required: ["spec"]