From 1adf9293acdf0e2cb14a4f5ba101adada63318fb Mon Sep 17 00:00:00 2001 From: kchiranjewee63 Date: Tue, 4 Jun 2024 17:10:22 -0400 Subject: [PATCH] Implement kubernetes mutating admission controller for tratteria sidecar injection --- kubernetes/deploy.sh | 8 ++ kubernetes/destroy.sh | 8 ++ kubernetes/spire/agent/agent-account.yaml | 5 + .../spire/agent/agent-cluster-role.yaml | 24 ++++ kubernetes/spire/agent/agent-configmap.yaml | 53 ++++++++ kubernetes/spire/agent/agent-daemonset.yaml | 71 +++++++++++ kubernetes/spire/deploy.sh | 59 +++++++++ kubernetes/spire/destroy.sh | 4 + kubernetes/spire/server/server-account.yaml | 5 + .../spire/server/server-cluster-role.yaml | 52 ++++++++ kubernetes/spire/server/server-configmap.yaml | 64 ++++++++++ kubernetes/spire/server/server-service.yaml | 14 +++ .../spire/server/server-statefulset.yaml | 63 ++++++++++ .../spire/server/spire-bundle-configmap.yaml | 5 + kubernetes/tratd/deploy.sh | 11 ++ kubernetes/tratd/deployment.yaml | 51 ++++++++ kubernetes/tratd/destroy.sh | 1 + kubernetes/tratd/service-account.yaml | 5 + kubernetes/tratd/service.yaml | 18 +++ ...teria-agent-injector-mutating-webhook.yaml | 25 ++++ rules/deploy-rules.sh | 27 ++++ .../order/verification-rules.ndjson | 2 +- .../stocks/verification-rules.ndjson | 2 +- .../example-rules}/trats.ndjson | 2 +- service/{ => api}/handler/handler.go | 6 +- .../pkg/apierrors}/apperrors.go | 2 +- service/api/pkg/rules/parse.go | 101 +++++++++++++++ service/{ => api}/pkg/rules/rules.go | 10 +- service/{ => api}/pkg/rules/types.go | 0 service/{ => api}/pkg/rules/validation.go | 0 service/{ => api}/pkg/service/service.go | 2 +- service/api/setup.go | 75 +++++++++++ service/cmd/main.go | 85 +++++-------- service/go.mod | 30 ++++- service/go.sum | 115 +++++++++++++++++ service/pkg/rules/parse.go | 116 ------------------ service/webhook/handler/handler.go | 92 ++++++++++++++ service/webhook/pkg/tlscreds/svidwatcher.go | 35 ++++++ service/webhook/pkg/tlscreds/tlscreds.go | 83 +++++++++++++ service/webhook/pkg/util/util.go | 45 +++++++ service/webhook/setup.go | 72 +++++++++++ 41 files changed, 1261 insertions(+), 187 deletions(-) create mode 100755 kubernetes/deploy.sh create mode 100755 kubernetes/destroy.sh create mode 100644 kubernetes/spire/agent/agent-account.yaml create mode 100644 kubernetes/spire/agent/agent-cluster-role.yaml create mode 100644 kubernetes/spire/agent/agent-configmap.yaml create mode 100644 kubernetes/spire/agent/agent-daemonset.yaml create mode 100755 kubernetes/spire/deploy.sh create mode 100755 kubernetes/spire/destroy.sh create mode 100644 kubernetes/spire/server/server-account.yaml create mode 100644 kubernetes/spire/server/server-cluster-role.yaml create mode 100644 kubernetes/spire/server/server-configmap.yaml create mode 100644 kubernetes/spire/server/server-service.yaml create mode 100644 kubernetes/spire/server/server-statefulset.yaml create mode 100644 kubernetes/spire/server/spire-bundle-configmap.yaml create mode 100755 kubernetes/tratd/deploy.sh create mode 100644 kubernetes/tratd/deployment.yaml create mode 100755 kubernetes/tratd/destroy.sh create mode 100644 kubernetes/tratd/service-account.yaml create mode 100644 kubernetes/tratd/service.yaml create mode 100644 kubernetes/tratd/tratteria-agent-injector-mutating-webhook.yaml create mode 100755 rules/deploy-rules.sh rename {example-rules => rules/example-rules}/order/verification-rules.ndjson (99%) rename {example-rules => rules/example-rules}/stocks/verification-rules.ndjson (99%) rename {example-rules => rules/example-rules}/trats.ndjson (99%) rename service/{ => api}/handler/handler.go (90%) rename service/{pkg/apperrors => api/pkg/apierrors}/apperrors.go (85%) create mode 100644 service/api/pkg/rules/parse.go rename service/{ => api}/pkg/rules/rules.go (80%) rename service/{ => api}/pkg/rules/types.go (100%) rename service/{ => api}/pkg/rules/validation.go (100%) rename service/{ => api}/pkg/service/service.go (91%) create mode 100644 service/api/setup.go delete mode 100644 service/pkg/rules/parse.go create mode 100644 service/webhook/handler/handler.go create mode 100644 service/webhook/pkg/tlscreds/svidwatcher.go create mode 100644 service/webhook/pkg/tlscreds/tlscreds.go create mode 100644 service/webhook/pkg/util/util.go create mode 100644 service/webhook/setup.go diff --git a/kubernetes/deploy.sh b/kubernetes/deploy.sh new file mode 100755 index 0000000..d52003f --- /dev/null +++ b/kubernetes/deploy.sh @@ -0,0 +1,8 @@ +cd spire +chmod +x deploy.sh +./deploy.sh +cd .. + +cd tratd +chmod +x deploy.sh +./deploy.sh \ No newline at end of file diff --git a/kubernetes/destroy.sh b/kubernetes/destroy.sh new file mode 100755 index 0000000..398a256 --- /dev/null +++ b/kubernetes/destroy.sh @@ -0,0 +1,8 @@ +cd tratd +chmod +x destroy.sh +./destroy.sh +cd .. + +cd spire +chmod +x destroy.sh +./destroy.sh \ No newline at end of file diff --git a/kubernetes/spire/agent/agent-account.yaml b/kubernetes/spire/agent/agent-account.yaml new file mode 100644 index 0000000..9091404 --- /dev/null +++ b/kubernetes/spire/agent/agent-account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: spire-agent + namespace: spire diff --git a/kubernetes/spire/agent/agent-cluster-role.yaml b/kubernetes/spire/agent/agent-cluster-role.yaml new file mode 100644 index 0000000..8bfe36c --- /dev/null +++ b/kubernetes/spire/agent/agent-cluster-role.yaml @@ -0,0 +1,24 @@ +# Required cluster role to allow spire-agent to query k8s API server +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: spire-agent-cluster-role +rules: +- apiGroups: [""] + resources: ["pods","nodes","nodes/proxy"] + verbs: ["get"] + +--- +# Binds above cluster role to spire-agent service account +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: spire-agent-cluster-role-binding +subjects: +- kind: ServiceAccount + name: spire-agent + namespace: spire +roleRef: + kind: ClusterRole + name: spire-agent-cluster-role + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/kubernetes/spire/agent/agent-configmap.yaml b/kubernetes/spire/agent/agent-configmap.yaml new file mode 100644 index 0000000..8a40c43 --- /dev/null +++ b/kubernetes/spire/agent/agent-configmap.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: spire-agent + namespace: spire +data: + agent.conf: | + agent { + data_dir = "/run/spire" + log_level = "DEBUG" + server_address = "spire-server" + server_port = "8081" + socket_path = "/run/spire/sockets/agent.sock" + trust_bundle_path = "/run/spire/bundle/bundle.crt" + trust_domain = "tratteria.io" + } + + plugins { + NodeAttestor "k8s_sat" { + plugin_data { + # NOTE: Change this to your cluster name + cluster = "docker-desktop" + } + } + + KeyManager "memory" { + plugin_data { + } + } + + WorkloadAttestor "k8s" { + plugin_data { + # Defaults to the secure kubelet port by default. + # Minikube does not have a cert in the cluster CA bundle that + # can authenticate the kubelet cert, so skip validation. + skip_kubelet_verification = true + node_name_env = "MY_NODE_NAME" + } + } + + WorkloadAttestor "unix" { + plugin_data { + } + } + } + + health_checks { + listener_enabled = true + bind_address = "0.0.0.0" + bind_port = "8080" + live_path = "/live" + ready_path = "/ready" + } diff --git a/kubernetes/spire/agent/agent-daemonset.yaml b/kubernetes/spire/agent/agent-daemonset.yaml new file mode 100644 index 0000000..980b0e8 --- /dev/null +++ b/kubernetes/spire/agent/agent-daemonset.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: spire-agent + namespace: spire + labels: + app: spire-agent +spec: + selector: + matchLabels: + app: spire-agent + template: + metadata: + namespace: spire + labels: + app: spire-agent + spec: + hostPID: true + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + serviceAccountName: spire-agent + initContainers: + - name: init + # This is a small image with wait-for-it, choose whatever image + # you prefer that waits for a service to be up. This image is built + # from https://github.com/lqhl/wait-for-it + image: cgr.dev/chainguard/wait-for-it + args: ["-t", "30", "spire-server:8081"] + containers: + - name: spire-agent + image: ghcr.io/spiffe/spire-agent:1.9.4 + args: ["-config", "/run/spire/config/agent.conf"] + env: + - name: MY_NODE_NAME + valueFrom: + fieldRef: + fieldPath: status.podIP + volumeMounts: + - name: spire-config + mountPath: /run/spire/config + readOnly: true + - name: spire-bundle + mountPath: /run/spire/bundle + - name: spire-agent-socket + mountPath: /run/spire/sockets + readOnly: false + livenessProbe: + httpGet: + path: /live + port: 8080 + failureThreshold: 2 + initialDelaySeconds: 15 + periodSeconds: 60 + timeoutSeconds: 3 + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: spire-config + configMap: + name: spire-agent + - name: spire-bundle + configMap: + name: spire-bundle + - name: spire-agent-socket + hostPath: + path: /run/spire/sockets + type: DirectoryOrCreate diff --git a/kubernetes/spire/deploy.sh b/kubernetes/spire/deploy.sh new file mode 100755 index 0000000..6267703 --- /dev/null +++ b/kubernetes/spire/deploy.sh @@ -0,0 +1,59 @@ +# Applying Spire Configurations +echo "\nApplying Spire Configurations...\n" + +kubectl create namespace spire + +# Create Server Bundle Configmap, Role & ClusterRoleBinding +kubectl apply \ + -f server/server-account.yaml \ + -f server/spire-bundle-configmap.yaml \ + -f server/server-cluster-role.yaml + +# Create Server Configmap +kubectl apply \ + -f server/server-configmap.yaml \ + -f server/server-statefulset.yaml \ + -f server/server-service.yaml + +# Configure and deploy the SPIRE Agent +kubectl apply \ + -f agent/agent-account.yaml \ + -f agent/agent-cluster-role.yaml + +kubectl apply \ + -f agent/agent-configmap.yaml \ + -f agent/agent-daemonset.yaml + +# Registering Workloads +echo "\nRegistering Workloads...\n" + +NAMESPACE=spire +POD_NAME=spire-server-0 + +echo "Waiting for spire server to be ready..." +while true; do + POD_STATUS=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.status.phase}') + READY=$(kubectl get pod $POD_NAME -n $NAMESPACE -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}') + if [[ "$POD_STATUS" == "Running" && "$READY" == "True" ]]; then + echo "Spire server is ready.\n" + break + else + echo "Waiting for spire server to be ready..." + sleep 5 + fi +done + +kubectl exec -n spire spire-server-0 -- \ + /opt/spire/bin/spire-server entry create \ + -spiffeID spiffe://tratteria.io/ns/spire/sa/spire-agent \ + -selector k8s_sat:cluster:docker-desktop \ + -selector k8s_sat:agent_ns:spire \ + -selector k8s_sat:agent_sa:spire-agent \ + -node + +kubectl exec -n spire spire-server-0 -- \ + /opt/spire/bin/spire-server entry create --dns tratd.tratteria.svc\ + -spiffeID spiffe://tratteria.io/tratteria \ + -parentID spiffe://tratteria.io/ns/spire/sa/spire-agent \ + -selector k8s:ns:tratteria \ + -selector k8s:sa:tratd-service-account diff --git a/kubernetes/spire/destroy.sh b/kubernetes/spire/destroy.sh new file mode 100755 index 0000000..3a808f0 --- /dev/null +++ b/kubernetes/spire/destroy.sh @@ -0,0 +1,4 @@ +# Destroying Spire Configurations +kubectl delete namespace spire +kubectl delete clusterrole spire-server-trust-role spire-agent-cluster-role +kubectl delete clusterrolebinding spire-server-trust-role-binding spire-agent-cluster-role-binding diff --git a/kubernetes/spire/server/server-account.yaml b/kubernetes/spire/server/server-account.yaml new file mode 100644 index 0000000..51ad4c5 --- /dev/null +++ b/kubernetes/spire/server/server-account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: spire-server + namespace: spire diff --git a/kubernetes/spire/server/server-cluster-role.yaml b/kubernetes/spire/server/server-cluster-role.yaml new file mode 100644 index 0000000..1d17e4e --- /dev/null +++ b/kubernetes/spire/server/server-cluster-role.yaml @@ -0,0 +1,52 @@ +# Role (namespace scoped) to be able to push certificate bundles to a configmap +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: spire-server-configmap-role + namespace: spire +rules: +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["patch", "get", "list"] +--- +# Binds above role to spire-server service account +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: spire-server-configmap-role-binding + namespace: spire +subjects: +- kind: ServiceAccount + name: spire-server + namespace: spire +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: spire-server-configmap-role +--- +# ClusterRole to allow spire-server node attestor to query Token Review API +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: spire-server-trust-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: ["tokenreviews"] + verbs: ["create"] +- apiGroups: ["admissionregistration.k8s.io"] + resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"] + verbs: ["get", "list", "patch", "watch"] +--- +# Binds above cluster role to spire-server service account +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: spire-server-trust-role-binding +subjects: +- kind: ServiceAccount + name: spire-server + namespace: spire +roleRef: + kind: ClusterRole + name: spire-server-trust-role + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/kubernetes/spire/server/server-configmap.yaml b/kubernetes/spire/server/server-configmap.yaml new file mode 100644 index 0000000..1d8444b --- /dev/null +++ b/kubernetes/spire/server/server-configmap.yaml @@ -0,0 +1,64 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: spire-server + namespace: spire +data: + server.conf: | + server { + bind_address = "0.0.0.0" + bind_port = "8081" + socket_path = "/tmp/spire-server/private/api.sock" + trust_domain = "tratteria.io" + data_dir = "/run/spire/data" + log_level = "DEBUG" + #AWS requires the use of RSA. EC cryptography is not supported + ca_key_type = "rsa-2048" + + ca_subject = { + country = ["US"], + organization = ["SPIFFE"], + common_name = "", + } + } + + plugins { + DataStore "sql" { + plugin_data { + database_type = "sqlite3" + connection_string = "/run/spire/data/datastore.sqlite3" + } + } + + NodeAttestor "k8s_sat" { + plugin_data { + clusters = { + # NOTE: Change this to your cluster name + "docker-desktop" = { + use_token_review_api_validation = true + service_account_allow_list = ["spire:spire-agent"] + } + } + } + } + + KeyManager "disk" { + plugin_data { + keys_path = "/run/spire/data/keys.json" + } + } + + Notifier "k8sbundle" { + plugin_data { + webhook_label = "tratteria.io/webhook" + } + } + } + + health_checks { + listener_enabled = true + bind_address = "0.0.0.0" + bind_port = "8080" + live_path = "/live" + ready_path = "/ready" + } diff --git a/kubernetes/spire/server/server-service.yaml b/kubernetes/spire/server/server-service.yaml new file mode 100644 index 0000000..fa4df2e --- /dev/null +++ b/kubernetes/spire/server/server-service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: spire-server + namespace: spire +spec: + type: NodePort + ports: + - name: grpc + port: 8081 + targetPort: 8081 + protocol: TCP + selector: + app: spire-server diff --git a/kubernetes/spire/server/server-statefulset.yaml b/kubernetes/spire/server/server-statefulset.yaml new file mode 100644 index 0000000..302aac5 --- /dev/null +++ b/kubernetes/spire/server/server-statefulset.yaml @@ -0,0 +1,63 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: spire-server + namespace: spire + labels: + app: spire-server +spec: + replicas: 1 + selector: + matchLabels: + app: spire-server + serviceName: spire-server + template: + metadata: + namespace: spire + labels: + app: spire-server + spec: + serviceAccountName: spire-server + containers: + - name: spire-server + image: ghcr.io/spiffe/spire-server:1.9.4 + args: + - -config + - /run/spire/config/server.conf + ports: + - containerPort: 8081 + volumeMounts: + - name: spire-config + mountPath: /run/spire/config + readOnly: true + - name: spire-data + mountPath: /run/spire/data + readOnly: false + livenessProbe: + httpGet: + path: /live + port: 8080 + failureThreshold: 2 + initialDelaySeconds: 15 + periodSeconds: 60 + timeoutSeconds: 3 + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: spire-config + configMap: + name: spire-server + volumeClaimTemplates: + - metadata: + name: spire-data + namespace: spire + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/kubernetes/spire/server/spire-bundle-configmap.yaml b/kubernetes/spire/server/spire-bundle-configmap.yaml new file mode 100644 index 0000000..4da3224 --- /dev/null +++ b/kubernetes/spire/server/spire-bundle-configmap.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: spire-bundle + namespace: spire \ No newline at end of file diff --git a/kubernetes/tratd/deploy.sh b/kubernetes/tratd/deploy.sh new file mode 100755 index 0000000..f7fbfae --- /dev/null +++ b/kubernetes/tratd/deploy.sh @@ -0,0 +1,11 @@ +docker build -t tratd:latest -f ../../service/Dockerfile ../../service/ + +kubectl create namespace tratteria +kubectl apply -f service-account.yaml +kubectl apply -f deployment.yaml +kubectl apply -f service.yaml +kubectl apply -f tratteria-agent-injector-mutating-webhook.yaml + +cd ../../rules +chmod +x deploy-rules.sh +./deploy-rules.sh example-rules \ No newline at end of file diff --git a/kubernetes/tratd/deployment.yaml b/kubernetes/tratd/deployment.yaml new file mode 100644 index 0000000..8001c12 --- /dev/null +++ b/kubernetes/tratd/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tratd + namespace: tratteria +spec: + replicas: 1 + selector: + matchLabels: + app: tratd + template: + metadata: + labels: + app: tratd + namespace: tratteria + spec: + dnsPolicy: ClusterFirstWithHostNet + serviceAccountName: tratd-service-account + containers: + - env: + - name: SPIFFE_ENDPOINT_SOCKET + value: unix:///run/spire/sockets/agent.sock + - name: TRATTERIA_SPIFFE_ID + value: spiffe://tratteria.io/tratteria + image: tratd + name: tratd + imagePullPolicy: Never + ports: + - containerPort: 9060 + protocol: TCP + - containerPort: 443 + protocol: TCP + volumeMounts: + - name: config-volume + mountPath: "/etc/rules" + readOnly: true + - mountPath: /run/spire/sockets + name: spire-agent-socket + readOnly: true + restartPolicy: Always + volumes: + - name: config-volume + configMap: + name: trats-rules-config + items: + - key: "trats-rules.ndjson" + path: "trats-rules.ndjson" + - name: spire-agent-socket + hostPath: + path: /run/spire/sockets + type: Directory diff --git a/kubernetes/tratd/destroy.sh b/kubernetes/tratd/destroy.sh new file mode 100755 index 0000000..7b9628e --- /dev/null +++ b/kubernetes/tratd/destroy.sh @@ -0,0 +1 @@ +kubectl delete namespace tratteria \ No newline at end of file diff --git a/kubernetes/tratd/service-account.yaml b/kubernetes/tratd/service-account.yaml new file mode 100644 index 0000000..529a115 --- /dev/null +++ b/kubernetes/tratd/service-account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: tratd-service-account + namespace: tratteria diff --git a/kubernetes/tratd/service.yaml b/kubernetes/tratd/service.yaml new file mode 100644 index 0000000..254108a --- /dev/null +++ b/kubernetes/tratd/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: tratd + namespace: tratteria +spec: + type: ClusterIP + ports: + - name: rules + port: 9060 + targetPort: 9060 + protocol: TCP + - name: webhook + port: 443 + targetPort: 443 + protocol: TCP + selector: + app: tratd diff --git a/kubernetes/tratd/tratteria-agent-injector-mutating-webhook.yaml b/kubernetes/tratd/tratteria-agent-injector-mutating-webhook.yaml new file mode 100644 index 0000000..5f2e02b --- /dev/null +++ b/kubernetes/tratd/tratteria-agent-injector-mutating-webhook.yaml @@ -0,0 +1,25 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: tratteria-agent-injector + labels: + tratteria.io/webhook: "true" +webhooks: + - name: tratteria-agent-injector.tratteria.com + clientConfig: + service: + name: tratd + namespace: tratteria + path: "/inject-tratteria-agents" + caBundle: + rules: + - operations: ["CREATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + admissionReviewVersions: ["v1", "v1beta1"] + sideEffects: None + timeoutSeconds: 5 + objectSelector: + matchLabels: + tratteria-agent: "true" diff --git a/rules/deploy-rules.sh b/rules/deploy-rules.sh new file mode 100755 index 0000000..ec6843b --- /dev/null +++ b/rules/deploy-rules.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +INPUT_DIRECTORY="$1" +CONFIG_MAP_NAME="trats-rules-config" +NAMESPACE="tratteria" + +if [ ! -d "$INPUT_DIRECTORY" ]; then + echo "Error: Trats rules directory $INPUT_DIRECTORY does not exist." + exit 1 +fi + +if [ $(find "$INPUT_DIRECTORY" -type f -name '*.ndjson' | wc -l) -eq 0 ]; then + echo "Error: No trats rules files found in $INPUT_DIRECTORY." + exit 1 +fi + +kubectl create configmap "$CONFIG_MAP_NAME" \ + --from-file=trats-rules.ndjson=<(find "$INPUT_DIRECTORY" -type f -name '*.ndjson' -exec cat {} + | sed 's/^/ /') \ + --namespace="$NAMESPACE" \ + --dry-run=client -o yaml | kubectl apply -f - + +echo -e "\nTrats rules deployed successfully." diff --git a/example-rules/order/verification-rules.ndjson b/rules/example-rules/order/verification-rules.ndjson similarity index 99% rename from example-rules/order/verification-rules.ndjson rename to rules/example-rules/order/verification-rules.ndjson index a4d06c1..5b216f0 100644 --- a/example-rules/order/verification-rules.ndjson +++ b/rules/example-rules/order/verification-rules.ndjson @@ -40,4 +40,4 @@ } } ] -} \ No newline at end of file +} diff --git a/example-rules/stocks/verification-rules.ndjson b/rules/example-rules/stocks/verification-rules.ndjson similarity index 99% rename from example-rules/stocks/verification-rules.ndjson rename to rules/example-rules/stocks/verification-rules.ndjson index 556f696..705eaee 100644 --- a/example-rules/stocks/verification-rules.ndjson +++ b/rules/example-rules/stocks/verification-rules.ndjson @@ -70,4 +70,4 @@ "traT-name": "portfolio-trat" } ] -} \ No newline at end of file +} diff --git a/example-rules/trats.ndjson b/rules/example-rules/trats.ndjson similarity index 99% rename from example-rules/trats.ndjson rename to rules/example-rules/trats.ndjson index 8620205..1c06c4f 100644 --- a/example-rules/trats.ndjson +++ b/rules/example-rules/trats.ndjson @@ -112,4 +112,4 @@ "adz-mapping": { "id": "${id}" } -} \ No newline at end of file +} diff --git a/service/handler/handler.go b/service/api/handler/handler.go similarity index 90% rename from service/handler/handler.go rename to service/api/handler/handler.go index 8548beb..aa33ba1 100644 --- a/service/handler/handler.go +++ b/service/api/handler/handler.go @@ -5,8 +5,8 @@ import ( "errors" "net/http" - "github.com/tratteria/tratd/pkg/apperrors" - "github.com/tratteria/tratd/pkg/service" + "github.com/tratteria/tratd/api/pkg/apierrors" + "github.com/tratteria/tratd/api/pkg/service" "go.uber.org/zap" ) @@ -35,7 +35,7 @@ func (h *Handlers) GetVerificationRulesHandler(w http.ResponseWriter, r *http.Re verificationRules, err := h.Service.GetVerificationRule(serviceName) if err != nil { - if errors.Is(err, apperrors.ErrVerificationRuleNotFound) { + if errors.Is(err, apierrors.ErrVerificationRuleNotFound) { http.Error(w, err.Error(), http.StatusNotFound) } else { http.Error(w, "Internal Server error", http.StatusInternalServerError) diff --git a/service/pkg/apperrors/apperrors.go b/service/api/pkg/apierrors/apperrors.go similarity index 85% rename from service/pkg/apperrors/apperrors.go rename to service/api/pkg/apierrors/apperrors.go index 38a91df..db6f286 100644 --- a/service/pkg/apperrors/apperrors.go +++ b/service/api/pkg/apierrors/apperrors.go @@ -1,4 +1,4 @@ -package apperrors +package apierrors import "errors" diff --git a/service/api/pkg/rules/parse.go b/service/api/pkg/rules/parse.go new file mode 100644 index 0000000..0a7c0c5 --- /dev/null +++ b/service/api/pkg/rules/parse.go @@ -0,0 +1,101 @@ +package rules + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" +) + +func parse(path string) (map[string]TraTDefinition, map[string]GenerationRule, map[string]map[string]VerificationRule, error) { + traTs := make(map[string]TraTDefinition) + generationRules := make(map[string]GenerationRule) + verificationRules := make(map[string]map[string]VerificationRule) + + file, err := os.Open(path) + if err != nil { + return nil, nil, nil, err + } + + defer file.Close() + + reader := bufio.NewReader(file) + decoder := json.NewDecoder(reader) + + for { + var jsonData map[string]interface{} + + if err := decoder.Decode(&jsonData); err == io.EOF { + break + } else if err != nil { + return nil, nil, nil, err + } + + switch jsonType := jsonData["type"]; jsonType { + case "TraT": + var definition TraTDefinition + + jsonBytes, err := json.Marshal(jsonData) + + if err != nil { + return nil, nil, nil, err + } + + if err := json.Unmarshal(jsonBytes, &definition); err != nil { + return nil, nil, nil, err + } + + traTs[definition.TraTName] = definition + + case "TraT-Generation-Rule": + var genRule GenerationRule + + jsonBytes, err := json.Marshal(jsonData) + + if err != nil { + return nil, nil, nil, err + } + + if err := json.Unmarshal(jsonBytes, &genRule); err != nil { + return nil, nil, nil, err + } + + key := genRule.Method + genRule.Route + + if _, exists := generationRules[key]; exists { + return nil, nil, nil, fmt.Errorf("multiple generation rules for route: %s, method: %s provided", genRule.Route, genRule.Method) + } + + generationRules[key] = genRule + + case "TraT-Verification-Rule": + var verRule VerificationRule + + jsonBytes, err := json.Marshal(jsonData) + + if err != nil { + return nil, nil, nil, err + } + + if err := json.Unmarshal(jsonBytes, &verRule); err != nil { + return nil, nil, nil, err + } + + serviceVerificatinRules, exists := verificationRules[verRule.Service] + if !exists { + serviceVerificatinRules = make(map[string]VerificationRule) + verificationRules[verRule.Service] = serviceVerificatinRules + } + + key := verRule.Method + verRule.Route + if _, exists := serviceVerificatinRules[key]; exists { + return nil, nil, nil, fmt.Errorf("multiple verification rules for service: %s, route: %s, method: %s provided", verRule.Service, verRule.Route, verRule.Method) + } + + serviceVerificatinRules[key] = verRule + } + } + + return traTs, generationRules, verificationRules, nil +} diff --git a/service/pkg/rules/rules.go b/service/api/pkg/rules/rules.go similarity index 80% rename from service/pkg/rules/rules.go rename to service/api/pkg/rules/rules.go index 9e91d7d..3aa2862 100644 --- a/service/pkg/rules/rules.go +++ b/service/api/pkg/rules/rules.go @@ -1,19 +1,17 @@ package rules import ( - "github.com/tratteria/tratd/pkg/apperrors" + "github.com/tratteria/tratd/api/pkg/apierrors" ) type Rules struct { - dir string traTs map[string]TraTDefinition generationRules map[string]GenerationRule verificationRules map[string]map[string]VerificationRule } -func NewRules(dir string) *Rules { +func NewRules() *Rules { return &Rules{ - dir: dir, traTs: make(map[string]TraTDefinition), generationRules: make(map[string]GenerationRule), verificationRules: make(map[string]map[string]VerificationRule), @@ -21,7 +19,7 @@ func NewRules(dir string) *Rules { } func (r *Rules) Load() error { - traTs, generationRules, verificationRules, err := parse(r.dir) + traTs, generationRules, verificationRules, err := parse("/etc/rules/trats-rules.ndjson") if err != nil { return err } @@ -44,7 +42,7 @@ func (r *Rules) Load() error { func (r *Rules) GetVerificationRules(service string) (map[string]VerificationRule, error) { verificationRule, exist := r.verificationRules[service] if !exist { - return nil, apperrors.ErrVerificationRuleNotFound + return nil, apierrors.ErrVerificationRuleNotFound } return verificationRule, nil diff --git a/service/pkg/rules/types.go b/service/api/pkg/rules/types.go similarity index 100% rename from service/pkg/rules/types.go rename to service/api/pkg/rules/types.go diff --git a/service/pkg/rules/validation.go b/service/api/pkg/rules/validation.go similarity index 100% rename from service/pkg/rules/validation.go rename to service/api/pkg/rules/validation.go diff --git a/service/pkg/service/service.go b/service/api/pkg/service/service.go similarity index 91% rename from service/pkg/service/service.go rename to service/api/pkg/service/service.go index f12415e..3b8406d 100644 --- a/service/pkg/service/service.go +++ b/service/api/pkg/service/service.go @@ -1,7 +1,7 @@ package service import ( - "github.com/tratteria/tratd/pkg/rules" + "github.com/tratteria/tratd/api/pkg/rules" "go.uber.org/zap" ) diff --git a/service/api/setup.go b/service/api/setup.go new file mode 100644 index 0000000..7a72192 --- /dev/null +++ b/service/api/setup.go @@ -0,0 +1,75 @@ +package api + +import ( + "fmt" + "log" + "net/http" + "time" + + "github.com/gorilla/mux" + "go.uber.org/zap" + + "github.com/tratteria/tratd/api/handler" + "github.com/tratteria/tratd/api/pkg/rules" + "github.com/tratteria/tratd/api/pkg/service" +) + +type API struct { + Router *mux.Router + Rules *rules.Rules + Handler *handler.Handlers + Logger *zap.Logger +} + +func Run() error { + logger, err := zap.NewProduction() + if err != nil { + return fmt.Errorf("cannot initialize Zap logger: %w", err) + } + + defer func() { + if err := logger.Sync(); err != nil { + log.Printf("Error syncing logger: %v", err) + } + }() + + rules := rules.NewRules() + + err = rules.Load() + if err != nil { + return fmt.Errorf("error loading rules: %w", err) + } + + service := service.NewService(rules, logger) + handler := handler.NewHandlers(service, logger) + + api := &API{ + Router: mux.NewRouter(), + Rules: rules, + Handler: handler, + Logger: logger, + } + + api.initializeRulesRoutes() + + srv := &http.Server{ + Handler: api.Router, + Addr: "0.0.0.0:9060", + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + logger.Info("Starting rules server on port 9060.") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Error("Failed to start the api server", zap.Error(err)) + + return err + } + + return nil +} + +func (a *API) initializeRulesRoutes() { + a.Router.HandleFunc("/verification-rules", a.Handler.GetVerificationRulesHandler).Methods("GET") + a.Router.HandleFunc("/generation-rules", a.Handler.GetGenerationRulesHandler).Methods("GET") +} diff --git a/service/cmd/main.go b/service/cmd/main.go index 576b887..28e4e05 100644 --- a/service/cmd/main.go +++ b/service/cmd/main.go @@ -1,74 +1,47 @@ package main import ( + "context" "log" - "net/http" "os" - "time" + "os/signal" + "syscall" - "github.com/gorilla/mux" - "go.uber.org/zap" - - "github.com/tratteria/tratd/handler" - "github.com/tratteria/tratd/pkg/rules" - "github.com/tratteria/tratd/pkg/service" + "github.com/tratteria/tratd/api" + "github.com/tratteria/tratd/webhook" ) -type App struct { - Router *mux.Router - Rules *rules.Rules - Logger *zap.Logger -} - func main() { - logger, err := zap.NewProduction() - if err != nil { - log.Fatalf("Cannot initialize Zap logger: %v.", err) - } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + setupSignalHandler(cancel) - defer func() { - if err := logger.Sync(); err != nil { - log.Printf("Error syncing logger: %v", err) + go func() { + log.Println("Starting API server...") + if err := api.Run(); err != nil { + log.Fatalf("API server failed: %v", err) } }() - if len(os.Args) < 2 { - logger.Error("Rules directory not provided. Please specify the rules directory as an argument when running the service.", - zap.String("usage", "tratd ")) - os.Exit(1) - } - - rulesDir := os.Args[1] - rules := rules.NewRules(rulesDir) - - err = rules.Load() - if err != nil { - logger.Fatal("Error loading rules:", zap.Error(err)) - } - - app := &App{ - Router: mux.NewRouter(), - Rules: rules, - Logger: logger, - } - - appService := service.NewService(app.Rules, app.Logger) - appHandler := handler.NewHandlers(appService, app.Logger) - - app.initializeRoutes(appHandler) + go func() { + log.Println("Starting Webhook server...") + if err := webhook.Run(); err != nil { + log.Fatalf("Webhook server failed: %v", err) + } + }() - srv := &http.Server{ - Handler: app.Router, - Addr: "0.0.0.0:9060", - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - } + <-ctx.Done() - logger.Info("Starting server on 9060.") - log.Fatal(srv.ListenAndServe()) + log.Println("Shutting down servers and controllers...") } -func (a *App) initializeRoutes(handlers *handler.Handlers) { - a.Router.HandleFunc("/verification-rules", handlers.GetVerificationRulesHandler).Methods("GET") - a.Router.HandleFunc("/generation-rules", handlers.GetGenerationRulesHandler).Methods("GET") +func setupSignalHandler(cancel context.CancelFunc) { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigs + cancel() + }() } diff --git a/service/go.mod b/service/go.mod index d4c822a..263ff09 100644 --- a/service/go.mod +++ b/service/go.mod @@ -4,10 +4,38 @@ go 1.22.0 require ( github.com/gorilla/mux v1.8.1 + github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 + github.com/spiffe/go-spiffe/v2 v2.2.0 go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.62.1 + k8s.io/api v0.30.1 + k8s.io/apimachinery v0.30.1 ) require ( - github.com/stretchr/testify v1.9.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/zeebo/errs v1.3.0 // indirect go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.18.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/service/go.sum b/service/go.sum index 5b733f3..640ae59 100644 --- a/service/go.sum +++ b/service/go.sum @@ -1,16 +1,131 @@ +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= +github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 h1:hQWBtNqRYrI7CWIaUSXXtNKR90KzcUA5uiuxFVWw7sU= +github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spiffe/go-spiffe/v2 v2.2.0 h1:9Vf06UsvsDbLYK/zJ4sYsIsHmMFknUD+feA7IYoWMQY= +github.com/spiffe/go-spiffe/v2 v2.2.0/go.mod h1:Urzb779b3+IwDJD2ZbN8fVl3Aa8G4N/PiUe6iXC0XxU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= +github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= +k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= +k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= +k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/service/pkg/rules/parse.go b/service/pkg/rules/parse.go deleted file mode 100644 index aafaed5..0000000 --- a/service/pkg/rules/parse.go +++ /dev/null @@ -1,116 +0,0 @@ -package rules - -import ( - "bufio" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" -) - -func parse(dir string) (map[string]TraTDefinition, map[string]GenerationRule, map[string]map[string]VerificationRule, error) { - traTs := make(map[string]TraTDefinition) - generationRules := make(map[string]GenerationRule) - verificationRules := make(map[string]map[string]VerificationRule) - - err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if !info.IsDir() { - file, err := os.Open(path) - if err != nil { - return err - } - - defer file.Close() - - reader := bufio.NewReader(file) - decoder := json.NewDecoder(reader) - - for { - var jsonData map[string]interface{} - - if err := decoder.Decode(&jsonData); err == io.EOF { - break - } else if err != nil { - return err - } - - switch jsonType := jsonData["type"]; jsonType { - case "TraT": - var definition TraTDefinition - - jsonBytes, err := json.Marshal(jsonData) - - if err != nil { - return err - } - - if err := json.Unmarshal(jsonBytes, &definition); err != nil { - return err - } - - traTs[definition.TraTName] = definition - - case "TraT-Generation-Rule": - var genRule GenerationRule - - jsonBytes, err := json.Marshal(jsonData) - - if err != nil { - return err - } - - if err := json.Unmarshal(jsonBytes, &genRule); err != nil { - return err - } - - key := genRule.Method + genRule.Route - - if _, exists := generationRules[key]; exists { - return fmt.Errorf("multiple generation rules for route: %s, method: %s provided", genRule.Route, genRule.Method) - } - - generationRules[key] = genRule - - case "TraT-Verification-Rule": - var verRule VerificationRule - - jsonBytes, err := json.Marshal(jsonData) - - if err != nil { - return err - } - - if err := json.Unmarshal(jsonBytes, &verRule); err != nil { - return err - } - - serviceVerificatinRules, exists := verificationRules[verRule.Service] - if !exists { - serviceVerificatinRules = make(map[string]VerificationRule) - verificationRules[verRule.Service] = serviceVerificatinRules - } - - key := verRule.Method + verRule.Route - if _, exists := serviceVerificatinRules[key]; exists { - return fmt.Errorf("multiple verification rules for service: %s, route: %s, method: %s provided", verRule.Service, verRule.Route, verRule.Method) - } - - serviceVerificatinRules[key] = verRule - } - } - } - - return nil - }) - - if err != nil { - return nil, nil, nil, err - } - - return traTs, generationRules, verificationRules, nil -} diff --git a/service/webhook/handler/handler.go b/service/webhook/handler/handler.go new file mode 100644 index 0000000..607d646 --- /dev/null +++ b/service/webhook/handler/handler.go @@ -0,0 +1,92 @@ +package handler + +import ( + "encoding/json" + "net/http" + + "github.com/tratteria/tratd/webhook/pkg/util" + + "go.uber.org/zap" + + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Handlers struct { + Logger *zap.Logger +} + +func NewHandlers(logger *zap.Logger) *Handlers { + return &Handlers{ + Logger: logger, + } +} + +func (h *Handlers) InjectTratteriaAgent(w http.ResponseWriter, r *http.Request) { + h.Logger.Info("Received Agent Injection Request") + + var admissionReview admissionv1.AdmissionReview + + if err := json.NewDecoder(r.Body).Decode(&admissionReview); err != nil { + h.Logger.Error("Failed to decode admission review", zap.Error(err)) + http.Error(w, "could not decode admission review", http.StatusBadRequest) + return + } + + if admissionReview.Request == nil { + h.Logger.Error("Received an AdmissionReview with no Request") + http.Error(w, "received an AdmissionReview with no Request", http.StatusBadRequest) + return + } + + admissionResponse := &admissionv1.AdmissionResponse{ + UID: admissionReview.Request.UID, + Allowed: true, + } + + var pod corev1.Pod + if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil { + h.Logger.Error("Could not unmarshal raw object into pod", zap.Error(err)) + admissionResponse.Result = &metav1.Status{ + Message: err.Error(), + } + } else { + patchOps, err := util.CreatePodPatch(&pod) + if err != nil { + h.Logger.Error("Could not create patch for pod", zap.Error(err)) + admissionResponse.Result = &metav1.Status{ + Message: err.Error(), + } + } else { + patchBytes, err := json.Marshal(patchOps) + if err != nil { + h.Logger.Error("Failed to marshal patch operations", zap.Error(err)) + admissionResponse.Result = &metav1.Status{ + Message: err.Error(), + } + } else { + admissionResponse.Patch = patchBytes + admissionResponse.PatchType = new(admissionv1.PatchType) + *admissionResponse.PatchType = admissionv1.PatchTypeJSONPatch + } + } + } + + responseAdmissionReview := admissionv1.AdmissionReview{ + Response: admissionResponse, + } + + responseAdmissionReview.TypeMeta = metav1.TypeMeta{ + Kind: "AdmissionReview", + APIVersion: "admission.k8s.io/v1", + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(responseAdmissionReview); err != nil { + h.Logger.Error("Failed to write response", zap.Error(err)) + http.Error(w, "failed to write response", http.StatusInternalServerError) + } + + h.Logger.Info("Agent Injection Request Processed Successfully", zap.Any("patched-pod", responseAdmissionReview)) +} diff --git a/service/webhook/pkg/tlscreds/svidwatcher.go b/service/webhook/pkg/tlscreds/svidwatcher.go new file mode 100644 index 0000000..4d0de7f --- /dev/null +++ b/service/webhook/pkg/tlscreds/svidwatcher.go @@ -0,0 +1,35 @@ +package tlscreds + +import ( + "log" + "os" + + "github.com/spiffe/go-spiffe/v2/workloadapi" +) + +type svidWatcher struct{} + +func (s *svidWatcher) OnX509ContextWatchError(err error) { + log.Printf("Error watching for updates: %v", err) +} + +func (s *svidWatcher) OnX509ContextUpdate(c *workloadapi.X509Context) { + pemCerts, pemKey, err := c.DefaultSVID().Marshal() + if err != nil { + log.Printf("Unable to marshal X.509 SVID: %v", err) + + return + } + + if err := os.WriteFile(CertPath, pemCerts, certsFileMode); err != nil { + log.Printf("Error writing certs file: %v", err) + + return + } + + if err := os.WriteFile(KeyPath, pemKey, keyFileMode); err != nil { + log.Printf("Error writing key file: %v", err) + + return + } +} diff --git a/service/webhook/pkg/tlscreds/tlscreds.go b/service/webhook/pkg/tlscreds/tlscreds.go new file mode 100644 index 0000000..18fdf3e --- /dev/null +++ b/service/webhook/pkg/tlscreds/tlscreds.go @@ -0,0 +1,83 @@ +package tlscreds + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "time" + + "github.com/spiffe/go-spiffe/v2/workloadapi" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const ( + CertsDirPath = "/etc/webhook/certs" + CertPath = "/etc/webhook/certs/tls.crt" + KeyPath = "/etc/webhook/certs/tls.key" + certsFileMode = os.FileMode(0o644) + keyFileMode = os.FileMode(0o600) + certsDirMode = os.FileMode(0o755) +) + +func SetupTLSCertAndKeyFromSPIRE() error { + spireClient, err := GetSPIREWorkLoadApiClient() + if err != nil { + return fmt.Errorf("unable to create spire workload api client: %w", err) + } + + if err := os.MkdirAll(CertsDirPath, certsDirMode); err != nil { + return err + } + + go func() { + defer spireClient.Close() + watcher := &svidWatcher{} + + err := spireClient.WatchX509Context(context.Background(), watcher) + if err != nil && status.Code(err) != codes.Canceled { + log.Fatalf("Error watching X.509 context: %v", err) + } + }() + + if err := WaitForCertificates(); err != nil { + return err + } + + return nil +} + +func GetSPIREWorkLoadApiClient() (*workloadapi.Client, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + spireClient, err := workloadapi.New(ctx) + if err != nil { + return nil, err + } + + return spireClient, nil +} + +func WaitForCertificates() error { + sleep := 500 * time.Millisecond + maxRetries := 360 + + for i := 1; i <= maxRetries; i++ { + time.Sleep(sleep) + + if _, err := os.Stat(CertPath); err != nil { + continue + } + + if _, err := os.Stat(KeyPath); err != nil { + continue + } + + return nil + } + + return errors.New("timed out waiting for trust bundle") +} diff --git a/service/webhook/pkg/util/util.go b/service/webhook/pkg/util/util.go new file mode 100644 index 0000000..6483e6e --- /dev/null +++ b/service/webhook/pkg/util/util.go @@ -0,0 +1,45 @@ +package util + +import ( + "encoding/json" + + "github.com/mattbaird/jsonpatch" + corev1 "k8s.io/api/core/v1" +) + +func CreatePodPatch(pod *corev1.Pod) ([]jsonpatch.JsonPatchOperation, error) { + var patch []jsonpatch.JsonPatchOperation + + serviceName := pod.Labels["app"] + + sidecar := corev1.Container{ + Name: "tratteria-agent", + Image: "tratteria-agent:latest", + Env: []corev1.EnvVar{ + { + Name: "SERVICE", + Value: serviceName, + }, + { + Name: "TRATTERIA_URL", + Value: "http://tratd.tratteria.svc.cluster.local:9060", + }, + }, + Ports: []corev1.ContainerPort{{ContainerPort: 80}}, + ImagePullPolicy: corev1.PullNever, + } + + sidecarJson, err := json.Marshal(sidecar) + if err != nil { + return nil, err + } + + patch = append(patch, jsonpatch.JsonPatchOperation{ + Operation: "add", + Path: "/spec/containers/-", + Value: json.RawMessage(sidecarJson), + }) + + return patch, nil +} + diff --git a/service/webhook/setup.go b/service/webhook/setup.go new file mode 100644 index 0000000..32a1253 --- /dev/null +++ b/service/webhook/setup.go @@ -0,0 +1,72 @@ +package webhook + +import ( + "fmt" + "log" + "net/http" + "time" + + "github.com/spiffe/go-spiffe/v2/workloadapi" + + "github.com/gorilla/mux" + "go.uber.org/zap" + + "github.com/tratteria/tratd/webhook/handler" + "github.com/tratteria/tratd/webhook/pkg/tlscreds" +) + +type Webhook struct { + Router *mux.Router + SpireWrokloadClient *workloadapi.Client + Handler *handler.Handlers + Logger *zap.Logger +} + +func Run() error { + logger, err := zap.NewProduction() + if err != nil { + return fmt.Errorf("cannot initialize Zap logger: %w", err) + } + + defer func() { + if err := logger.Sync(); err != nil { + log.Printf("Error syncing logger: %v", err) + } + }() + + handler := handler.NewHandlers(logger) + + webhook := &Webhook{ + Router: mux.NewRouter(), + Handler: handler, + Logger: logger, + } + + webhook.initializeRoutes() + + srv := &http.Server{ + Handler: webhook.Router, + Addr: "0.0.0.0:443", + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + if err := tlscreds.SetupTLSCertAndKeyFromSPIRE(); err != nil { + logger.Error("Error setting up TLS creds", zap.Error(err)) + + return err + } + + logger.Info("Starting webhook server with TLS on port 443") + if err := srv.ListenAndServeTLS(tlscreds.CertPath, tlscreds.KeyPath); err != nil && err != http.ErrServerClosed { + logger.Error("Failed to start the webhook server", zap.Error(err)) + + return err + } + + return nil +} + +func (w *Webhook) initializeRoutes() { + w.Router.HandleFunc("/inject-tratteria-agents", w.Handler.InjectTratteriaAgent).Methods("POST") +}