diff --git a/charts/identity-gatekeeper/.helmignore b/charts/identity-gatekeeper/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/identity-gatekeeper/.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/charts/identity-gatekeeper/Chart.yaml b/charts/identity-gatekeeper/Chart.yaml new file mode 100644 index 0000000..1403a79 --- /dev/null +++ b/charts/identity-gatekeeper/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: identity-gatekeeper +description: Policy enforcement integration with Keycloak + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 1.0.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "2.8.0" diff --git a/charts/identity-gatekeeper/templates/_helpers.tpl b/charts/identity-gatekeeper/templates/_helpers.tpl new file mode 100644 index 0000000..65872a2 --- /dev/null +++ b/charts/identity-gatekeeper/templates/_helpers.tpl @@ -0,0 +1,82 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "identity-gatekeeper.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 "identity-gatekeeper.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 "identity-gatekeeper.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "identity-gatekeeper.labels" -}} +helm.sh/chart: {{ include "identity-gatekeeper.chart" . }} +{{ include "identity-gatekeeper.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Values.deployment.image.tag | default .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "identity-gatekeeper.selectorLabels" -}} +app.kubernetes.io/name: {{ include "identity-gatekeeper.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "identity-gatekeeper.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "identity-gatekeeper.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Return the appropriate apiVersion for ingress +*/}} +{{- define "identity-gatekeeper.ingress.apiVersion" -}} +{{- if semverCompare "<1.14-0" (include "identity-gatekeeper.kubeVersion" $) -}} +{{- print "extensions/v1beta1" -}} +{{- else if semverCompare "<1.19-0" (include "identity-gatekeeper.kubeVersion" $) -}} +{{- print "networking.k8s.io/v1beta1" -}} +{{- else -}} +{{- print "networking.k8s.io/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the target Kubernetes version +*/}} +{{- define "identity-gatekeeper.kubeVersion" -}} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersionOverride }} +{{- end -}} \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/configmap.yaml b/charts/identity-gatekeeper/templates/configmap.yaml new file mode 100644 index 0000000..071a57c --- /dev/null +++ b/charts/identity-gatekeeper/templates/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "identity-gatekeeper.fullname" . }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + namespace: {{ .Release.Namespace }} +data: + config.yaml: | + {{- tpl (.Values.config | toYaml) $ | nindent 4 }} \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/deployment.yaml b/charts/identity-gatekeeper/templates/deployment.yaml new file mode 100644 index 0000000..49bc3f2 --- /dev/null +++ b/charts/identity-gatekeeper/templates/deployment.yaml @@ -0,0 +1,117 @@ +{{- $adminPort := regexFind ":[0-9]+" (index .Values.config "listen-admin") | trimPrefix ":" -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "identity-gatekeeper.name" . }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + namespace: {{ .Release.Namespace }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.deployment.replicaCount }} + {{- end }} + strategy: + {{- toYaml .Values.deployment.strategy | nindent 4 }} + selector: + matchLabels: + {{- include "identity-gatekeeper.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ print .Values.config | sha256sum | quote }} + {{- if and (index .Values.config "enable-metrics") .Values.metrics.addPrometheusScrapeAnnotation }} + prometheus.io/path: "/oauth/metrics" + prometheus.io/port: {{ $adminPort | quote }} + prometheus.io/scrape: "true" + {{- end }} + {{- with .Values.deployment.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "identity-gatekeeper.selectorLabels" . | nindent 8 }} + {{- range $key, $value := .Values.deployment.podLabels }} + {{ $key }}: {{ $value | quote }} + {{- end }} + spec: + {{- with .Values.deployment.image.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + automountServiceAccountToken: {{ .Values.deployment.automountServiceAccountToken }} + serviceAccountName: {{ include "identity-gatekeeper.serviceAccountName" . }} + {{- if .Values.deployment.podSecurityContext.enabled }} + securityContext: + {{- omit .Values.deployment.podSecurityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- if .Values.deployment.containerSecurityContext.enabled }} + securityContext: + {{- omit .Values.deployment.containerSecurityContext "enabled" | toYaml | nindent 12 }} + {{- end }} + image: "{{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.deployment.image.pullPolicy }} + args: + - --config + - /etc/gatekeeper/config.yaml + {{- with .Values.deployment.extraArgs }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.deployment.extraEnvVars }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + envFrom: + {{- with .Values.deployment.extraEnvFrom }} + {{- tpl . $ | nindent 12 }} + {{- end }} + - secretRef: + name: {{ include "identity-gatekeeper.name" . }} + ports: + - name: proxy + containerPort: {{ regexFind ":[0-9]+" .Values.config.listen | trimPrefix ":" }} + protocol: TCP + - name: admin + containerPort: {{ $adminPort }} + protocol: TCP + {{- if .Values.deployment.livenessProbe.enabled }} + livenessProbe: + {{- omit .Values.deployment.livenessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.deployment.readinessProbe.enabled }} + readinessProbe: + {{- omit .Values.deployment.readinessProbe "enabled" | toYaml | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.deployment.resources | nindent 12 }} + volumeMounts: + - mountPath: /etc/gatekeeper + name: config + {{- with .Values.deployment.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + - configMap: + name: {{ include "identity-gatekeeper.fullname" . }} + name: config + {{- with .Values.deployment.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.deployment.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- with .Values.deployment.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.deployment.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.deployment.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.deployment.hostAliases }} + hostAliases: {{ toYaml .Values.deployment.hostAliases | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/hpa.yaml b/charts/identity-gatekeeper/templates/hpa.yaml new file mode 100644 index 0000000..3af673a --- /dev/null +++ b/charts/identity-gatekeeper/templates/hpa.yaml @@ -0,0 +1,29 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "identity-gatekeeper.fullname" . }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + namespace: {{ .Release.Namespace }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "identity-gatekeeper.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/ingress.yaml b/charts/identity-gatekeeper/templates/ingress.yaml new file mode 100644 index 0000000..6ddb293 --- /dev/null +++ b/charts/identity-gatekeeper/templates/ingress.yaml @@ -0,0 +1,51 @@ +{{- if .Values.ingress.enabled -}} +{{- $name := include "identity-gatekeeper.name" . -}} +{{- $svcPort := .Values.service.proxy.port -}} +apiVersion: {{ include "identity-gatekeeper.ingress.apiVersion" . }} +kind: Ingress +metadata: + name: {{ $name }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + namespace: {{ .Release.Namespace }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" (include "identity-gatekeeper.kubeVersion" $)) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" (include "identity-gatekeeper.kubeVersion" $)) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" (include "identity-gatekeeper.kubeVersion" $) }} + service: + name: {{ $name }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $name }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/pdb.yaml b/charts/identity-gatekeeper/templates/pdb.yaml new file mode 100644 index 0000000..02fba8d --- /dev/null +++ b/charts/identity-gatekeeper/templates/pdb.yaml @@ -0,0 +1,14 @@ +{{- if .Values.pdb.create }} +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: {{ include "identity-gatekeeper.fullname" . }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + namespace: {{ .Release.Namespace }} +spec: + minAvailable: {{ .Values.pdb.minAvailable }} + selector: + matchLabels: + {{- include "identity-gatekeeper.selectorLabels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/sealedsecret.yaml b/charts/identity-gatekeeper/templates/sealedsecret.yaml new file mode 100644 index 0000000..7310093 --- /dev/null +++ b/charts/identity-gatekeeper/templates/sealedsecret.yaml @@ -0,0 +1,11 @@ +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: {{ include "identity-gatekeeper.name" . }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + namespace: {{ .Release.Namespace }} +spec: + encryptedData: + PROXY_CLIENT_SECRET: "{{ .Values.secrets.clientSecret }}" + PROXY_ENCRYPTION_KEY: "{{ .Values.secrets.encryptionKey }}" \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/service.yaml b/charts/identity-gatekeeper/templates/service.yaml new file mode 100644 index 0000000..c12b360 --- /dev/null +++ b/charts/identity-gatekeeper/templates/service.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "identity-gatekeeper.name" . }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + namespace: {{ .Release.Namespace }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.proxy.port }} + targetPort: proxy + protocol: TCP + name: proxy + {{- if eq .Values.service.type "NodePort" }} + nodePort: {{ .Values.service.proxy.nodePort }} + {{- end }} + - port: {{ .Values.service.admin.port }} + targetPort: admin + protocol: TCP + name: admin + {{- if eq .Values.service.type "NodePort" }} + nodePort: {{ .Values.service.admin.nodePort }} + {{- end }} + selector: + {{- include "identity-gatekeeper.selectorLabels" . | nindent 4 }} \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/serviceaccount.yaml b/charts/identity-gatekeeper/templates/serviceaccount.yaml new file mode 100644 index 0000000..6ef1d48 --- /dev/null +++ b/charts/identity-gatekeeper/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "identity-gatekeeper.serviceAccountName" . }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + namespace: {{ .Release.Namespace }} +{{- end }} \ No newline at end of file diff --git a/charts/identity-gatekeeper/templates/servicemonitor.yaml b/charts/identity-gatekeeper/templates/servicemonitor.yaml new file mode 100644 index 0000000..5a2cf5a --- /dev/null +++ b/charts/identity-gatekeeper/templates/servicemonitor.yaml @@ -0,0 +1,27 @@ +{{- if and (index .Values.config "enable-metrics") .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "identity-gatekeeper.fullname" . }} + {{- with .Values.metrics.serviceMonitor.namespace }} + namespace: {{ . }} + {{- end }} + labels: + {{- include "identity-gatekeeper.labels" . | nindent 4 }} + {{- with .Values.metrics.serviceMonitor.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + - port: admin + {{- with .Values.metrics.serviceMonitor.interval }} + interval: {{ . }} + {{- end }} + path: /oauth/metrics + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "identity-gatekeeper.selectorLabels" . | nindent 6 }} +{{- end }} \ No newline at end of file diff --git a/charts/identity-gatekeeper/values.yaml b/charts/identity-gatekeeper/values.yaml new file mode 100644 index 0000000..a257ef2 --- /dev/null +++ b/charts/identity-gatekeeper/values.yaml @@ -0,0 +1,134 @@ +nameOverride: "" +fullnameOverride: "" +kubeVersionOverride: "" +deployment: + replicaCount: 1 + image: + pullPolicy: IfNotPresent + strategy: + type: Recreate + # rollingUpdate: + # maxSurge: 25% + # maxUnavailable: 25% + automountServiceAccountToken: false + podAnnotations: {} + podLabels: {} + podSecurityContext: + enabled: false + # fsGroup: 2000 + containerSecurityContext: + enabled: true + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + # runAsUser: 1000 + extraArgs: [] + # - --foo + # - --bar + extraEnvVars: [] + # - name: FOO + # value: BAR + extraVolumes: [] + # - emptyDir: {} + # name: tmp + extraVolumeMounts: [] + # - mountPath: /tmp + # name: tmp + priorityClassName: "" + hostAliases: [] + # - ip: "127.0.0.1" + # hostnames: + # - "foo.local" + # - "bar.local" + # - ip: "10.1.2.3" + # hostnames: + # - "foo.remote" + # - "bar.remote" + resources: + limits: {} + # cpu: 100m + # memory: 128Mi + requests: {} + # cpu: 100m + # memory: 128Mi + nodeSelector: {} + tolerations: [] + affinity: {} + livenessProbe: + enabled: false + httpGet: + path: /oauth/health + port: admin + readinessProbe: + enabled: true + httpGet: + path: /oauth/health + port: admin +service: + type: ClusterIP + annotations: {} + proxy: + port: 3000 + nodePort: + admin: + port: 4000 + nodePort: +serviceAccount: + create: true + annotations: {} + name: "" +ingress: + enabled: true + className: "" + annotations: + cert-manager.io/cluster-issuer: letsencrypt + hosts: + - host: identity.gatekeeper.develop.eoepca.org + paths: + - path: / + pathType: Prefix + tls: + - secretName: identity-gatekeeper-tls-certificate + hosts: + - identity.gatekeeper.develop.eoepca.org +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 + #targetMemoryUtilizationPercentage: 80 +pdb: + create: false + minAvailable: 1 +metrics: + addPrometheusScrapeAnnotation: false + serviceMonitor: + enabled: false + namespace: um + additionalLabels: {} + annotations: {} + interval: +config: + client-id: dummy-service + discovery-url: https://identity.keycloak.develop.eoepca.org/realms/master + no-redirects: true + no-proxy: true + enable-uma: true + #enable-default-deny: true + cookie-domain: develop.eoepca.org + cookie-access-name: auth_user_id + cookie-refresh-name: auth_refresh_token + #secure-cookie: true + enable-metrics: true + enable-logging: true + enable-request-id: true + enable-login-handler: true + enable-refresh-tokens: true + enable-logout-redirect: true + listen: :3000 + listen-admin: :4000 +secrets: + clientSecret: AgAPJeLi8svAkTu0q3gu+bfeovANZqbqWsn66dy3FWLOIYIvca2uy+3E/cXLc3iVjlH5maF6NEobB7URLgR7doNHnjs9s6uzHYxxUzp41dx3F7u13KJEdeHk12R+mr0xFsv+viACwki+XL1RzJtWOTv1VTJV0QHf0jqylgvjGnxvH45yk9AqWxBZPQzIjI2/y6HtmTbJAVFqnUz/3cznPQAibX3f0Re4iSjWDiv5QpfTguGRyg1caP3ngby3L8HINPjMpuGXWDxWzji3ciRxoL0/C3Y6mK1JhR80pQxP4G3entonClU0KdcHEjlsDkHtoqqpoTmTkl1gjwurUeZ5+3W2/GXuI0df+jyLG4SE9rXa0PWexgo3AiiBnAjAkFW/g4/ITWkWmkvEH5Iwv13eLWYMxZxm5zm47GXPeJEllgRLzrPCkd16h+fQ0SLQmh/dNFyTGZcknpZFpe+Tcoxa71rzHtYSZN35XLqFfS77m/O/UE0eIdjbtt70sNgEQIK/brBCmcSElfZ5aiKgbFU2yhabLVYZ93PXM2pPJns3IjSmRiJXS639kEJ8BywU5GVPoATG5hgpew3OpYEaO3gg1LD9kkH8caj+9palaBUhBQ8UJgRbeO3k5Fi/BTueoDk1XY7tVV7CuxZPMfJNcJsQxIRnxxMONNfLFWI0LPvMI/kkOkfZtyJsho8sFiHGOnJS+yMocK9OJAoepJ/fagUHdL/T71lSIEcfx+q4K7Uc3UNwQQ== + encryptionKey: AgBMu2WaXmyhciLf45Q+1kc6AyS3HSZyshDyzMlJVMMZw9lwAP9rUxmVKnVLKsAN8Kyyh6veVhBH7k3kzP1xeCrGN3ck30ZnmCgCTlH8QnEsg1SLaTfoln4MLZBPyi30oMcJuF8V2aWphv42dChopKoYCaOMW+11YG7/CTKsNbAugg4bS3CAG8cTzcLO8oyzXPsT8KMH+Jvj4U6ASpLsUG8S6tazBAqGLgiOeanLUcxNynsVr0XjwV6/lqJacX8Er8eDK8jiP/EiwZ7SmazPQ0FsUxCeQ5ogR3Pmz3d2KHgBoAKD05ohHhUa79gdIc6toF2gv0KvE2529zmBZIuoABqXjXio/V4ySMZE4lYGVMUcW9kwpskUhCkGbMusdD2B+EL+0cHaVSM6b70eUv1mLoySe4wYj7WX4EvxWVa4dk2JHOyzTnJh7x2jpLUsVszZbVL7J2lm4Ne7CdAxeTLAa/mqB5kqdTL0MoORLPgZmNGWtFrxLbSCe/TobYvJpUqit4t++AErxKvKzuXBB81nrO5sL5ZwCxdKHAVksbsveGfjesgBqJs50a8pfzcZJCg/sK/OWm2TQU0YyCJ+lZMp7pcrZ//CPJCc5yu96L1GiW+LZya3xE+G6Vq90glbMO7ss+FbI6sDbA0xYw/VS3kMqZnkPUmGlKCN61ECEhenKCNQItYAEkFlo+Z4vp11V71nS+EO30rUiMH9O5yFwxEyFPKpLDjryuwsaYReNFlcZyz+9g== \ No newline at end of file