1
0
Fork 0

xactmon: Deploy xactmon

`xactmon` is a new tool I developed to parse transaction notifications
from banks and automatically import them into my personal finance
tracker.  It is designed in a modular fashion, composed of three main
components:

* Receiver
* Processor
* Importer

Components communicate with one another using an AMQP exchange.
Hypothetically, there could be multipel implementations of the receiver
and importer components.  Right now, there is only a JMAP receiver,
which fetches email messages (from Fastmail), and a Firefly III
importer.  The processor is a singleton, handling notifications from the
receiver, parsing them into a normalized format, and passing them on to
the importer.  It uses a set of rules to decide how to parse the
messages, and supports using either a regular expression with named
capture groups or an Awk script to extract the relevant information.
etcd
Dustin 2024-07-22 08:11:31 -05:00
parent ccc46288c2
commit a04a2b5334
8 changed files with 405 additions and 0 deletions

21
xactmon/certificate.yaml Normal file
View File

@ -0,0 +1,21 @@
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: rabbitmq
spec:
secretName: rabbitmq-cert
commonName: xactmon
issuerRef:
group: cert-manager.io
kind: ClusterIssuer
name: rabbitmq-ca
privateKey:
algorithm: ECDSA
rotationPolicy: Always
keystores:
pkcs12:
create: true
profile: Modern2023
passwordSecretRef:
name: rabbitmq-cert-password
key: password

22
xactmon/commerce.awk Normal file
View File

@ -0,0 +1,22 @@
/ending in/ {
gsub(/^ +/, "");
account=$0
}
/•/ {
if (date=="") {
gsub(/[^0-9-]+/, "");
date=$0
} else if (description == "") {
gsub(/.*• /, "")
description=$0
} else if (amount == "") {
gsub(/[^0-9.]+/, "");
amount=$0
}
}
END {
OFS="|"
print(account, date, amount, description)
}

16
xactmon/config.toml Normal file
View File

@ -0,0 +1,16 @@
processor_rules = "/etc/xactmon/rules.toml"
[jmap]
url = "https://api.fastmail.com"
token_file = "/run/secrets/xactmon/fastmail.token"
[amqp]
url = "amqps://xactmon@rabbitmq.pyrocufflink.blue?auth_mechanism=external"
clientcert = "/run/secrets/rabbitmq/cert/keystore.p12"
clientcert_password = "/run/secrets/rabbitmq/password"
cacert = "/run/dch-ca/dch-root-ca.crt"
[firefly]
url = "https://firefly.pyrocufflink.blue"
token_file = "/run/secrets/xactmon/firefly.token"
error_if_duplicate_hash = false

View File

@ -0,0 +1,27 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: xactmon
labels:
- pairs:
app.kubernetes.io/instance: xactmon
app.kubernetes.io/part-of: xactmon
resources:
- namespace.yaml
- secrets.yaml
- xactmon.yaml
- certificate.yaml
- ../dch-root-ca
configMapGenerator:
- name: xactmon
files:
- config.toml
- rules.toml
- commerce.awk
images:
- name: git.pyrocufflink.net/packages/xactmon
newTag: dev

7
xactmon/namespace.yaml Normal file
View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: xactmon
labels:
app.kubernetes.io/component: xactmon
app.kubernetes.io/name: xactmon

19
xactmon/rules.toml Normal file
View File

@ -0,0 +1,19 @@
[[rule]]
name = "Commerce Bank"
match = "commercebankalerts@commercebank.com"
date_fmt = "%m-%d-%Y"
awk_script = "/etc/xactmon/commerce.awk"
[[rule]]
name = "Chase (Amazon Rewards) Visa"
match = "no.reply.alerts@chase.com"
date_fmt = "%b %d, %Y at %-I:%M %p"
regex = 'Account\s*(?P<account>.+)\n\s*Date\s+(?P<date>.+[AP]M).*\n\s*Merchant\s+(?P<description>.+)\n\s*Amount\s+\$(?P<amount>[0-9]+\.[0-9]{2})'
[[rule]]
name = "HSA Bank"
match = "hsabank.com"
date_fmt = "%m/%d/%Y"
account = "HSA Bank HSA"
default_description = "Debit Card Purchase"
regex = '\$(?P<amount>[0-9]+\.[0-9]{2}) on (?P<date>[0-9]{1,2}/[0-9]{1,2}/[0-9]{4}).'

55
xactmon/secrets.yaml Normal file
View File

@ -0,0 +1,55 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: rabbitmq-cert-password
namespace: xactmon
labels:
app.kubernetes.io/name: xactmon
app.kubernetes.io/component: xactmon
spec:
encryptedData:
password: AgCblkGzJTe+UaKxD9fGRPidDlULnhoqJ8Xvp0Cyl7jNx9Yky2B3SVrC48aCgTNUcRTAahzXCJ8jNUefVbN/x87z/Dim6PepZsP0lBI5qhdW9SZnlrwInObbaCiHWp1SU/WFyxxU4asxWvPArdzfEAe2/V1eulC7kWjNRGXemfbWhY/qL/5jBJhgoD/0xENEa1OxRqDo3EpqFlgKWGm9Sr9F1V+PpksZCgZkXfI/V3Y2v5s9z0s+Ylc444kwvLjkK5dmXh1FnqjK/ZeXwjZUXg+cjZGVy5bNJYlhxRURY1DT0BAvShTJCCH2RajmbV2rZf90v65TcNHyURGA9jKXmFd+x7J9C0jdnN2bhmWWreQlfXB6WZCoU+G1t06nNVIOeS8p5BIByvenWCwqklnHYCzUxu0PEN86dTqSGWD9r8Y4VE94PZQ+wO+MmGEn9y+CUNKJ5apoP3AhpW0rliShUAEjQnyOOekgPYETNSwvb9IkKiueXc3NztQNch39X97kVsqVkknpay2GdxcuCdDZLqFoUpzyhaJo7QQxhZBiN1oW2Wy+m+CIM1gzi0U2f91Gz53/ozhlGD92QZqhd55MwGI4sANyyIo4TRxgV//VKDA/uIpJcDe0jnGxxbvg5qLHqSl+87jaTA6h1cUWAyVrzX5dJgjthc6h25Azne6YZHIItY17Hul2eLcEGAmdvu6dYWJ6SzYC46W997ndtdClhBwV
template:
metadata:
name: rabbitmq-cert-password
namespace: xactmon
labels:
app.kubernetes.io/name: xactmon
app.kubernetes.io/component: xactmon
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: xactmon
namespace: xactmon
labels:
app.kubernetes.io/name: xactmon
app.kubernetes.io/component: xactmon
spec:
encryptedData:
fastmail.token: AgB7GUTJ4QqkLDUy2DePEb3OLtKNik6xw46oe2WA0HC37CEG02KREFK4vJFcbUAwkqpGO51FYWq1JmvKxzIse9265N98Dl1D0w9AVLKBBuSAT915DK6W8ya9zVLgYlPBnhlBZzwRjwSN3i5dREbvIGtmnZseZYgXCWuE1JRGbd/HXDOnPSq98rM/XtDUZ+p8x40LpC1rYAVmTHrDoLOHM1Gyt6X6jR6jifWtmOXoPFE4VDdidaIhmHa7mJoboHuaH+8QSlCrwqw6aG5EVJ1GzKbCaWiVjSgGFLVJjHJRJHUQTa48vlDhMlPOgSQ5ur2VMuBaw+9FpjVukL4pT/GCNAAXpotkx/EQg3iVJsboC3D5Xt5P92KbJpZrzH8EHZg5mLNz7rUOcj4Q5LHdECmlrOsLLXAWtc2u0eTo/28V8ZaZZgORWCbsHom8ziaS2txMQP3S2uUIH7g67kRLuD91nw/n3sCaxrJtDsHnvvLkanCdooPPzyMRrVqu0OP4nbzWeCoziI8JLjp++RZ56Ztzik9PtpqHnJERedShH/GXD0P5B7oTPf1qbv3Z3w4N/ujXSmIxK0RvwBDqgzfzyRHQkzxq4EK4Y98KKEjQHM5bgD3lreuIw+mSvBS4qoZGse4LkCMNOdcm0qDGeVnhbE54a36USqpPte1dxyvrL61vaT7HZDTVV0ib8c/YJ3sYz8jrt9By7cXtjyVWD0j+m2Jb22ZrqvnwpW5mgC3O+C2maRTuaZd8s8E2Qr0RM4mPUA+jXHP4mBPhGDkO2x378vFUA/u9OPToTDSYl6H9ZaLvyITWhs4=
firefly.token: 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
template:
metadata:
name: xactmon
namespace: xactmon
labels:
app.kubernetes.io/name: xactmon
app.kubernetes.io/component: xactmon
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: imagepull-gitea
namespace: xactmon
spec:
encryptedData:
.dockerconfigjson: AgCrayJ/+8ndjOvkl/zocR4KAkVd1e7Pab48ngQS1ePwj6PA3xuLaL4xacS2QUoGyNrh4ATdsH4mw7dscVFDTFd7UWiQuA+Xx4tFcPX+NNd31QPp+MgdZtmbmBAPQEf5eiIXEsuQB++TP/EuOgS1pSKPH40LJOsNHqGwCZ+l/tQs/Ll+fZ4VqaiSALMGrJICeCO3k7Gzz03glqVu2oKxIzKqoYrv0+G+UWqaDCyJsjoT5KYs2nJOm/ArbqrUQJBqdjKf4QO07krZcSPHrw9a49VGnfx6ILifUJJ/xw4hbqNt7rmuFknfALDtHj1z0vY5ZX+45NcXjRF+9JZd/2McjgEtHWaOE6GHKG//nciNWbWXwmLZ/+2tphBa9pg6FiNSQhepKGTLQYNmkHbcTevvqW6e7bFA7QmV6yXZ/u2WwKvXtgZUVCmpD9qnltJtI/oiFKnkle4Y/lOtSN/fTo7uI8YT5u+qEtKJEwv0Txe2rFL74xzjC9eaI6ehniksk1CMlVTi66Y4AvbZSzGYGwbMvUfl62hzLPSogORxGcHcql6HCcsdXfwQU6etmVba29ZS7CkB/+pfu4UO43mnnT33KXIzZknTiQKruAzGKLCN/nHGBilTNYWiTTPCje3WhLMnfzJ58w99ncK4yAOg8xk/qekWoyJNSofAc4VVruaHgHXh0QC8NX5jhzQkrk/fAtPd8U3GLXV5xflpwL2M/liVHkoQNIJc9n7tuCNTYFcu58pC5Zxqpi0PNkm9PUxDYjRfIDE9TcHnMK3ma4REA5aGYokzljv28qxC0X7WKkZ4cpYMAvFQ1Ipq8EeSdBUiE8pTTkBZ2J1JwsqepcwYt/w3+5wRd3tFf5BOLxEiXC5nM8KubnoWXaSszyDNVhRqtGxuzqrWdhhT5iJXd2aiiMKJxnsCzNcQKpo0UP+s9utzCDMFxKkVGeld/FZhv5SUkUugwexvjX7zfcOPX0C841P5uDu2saZFJR1oaNwPnku5yVPvrn6AYGxaZQvjol6/qgI=
template:
metadata:
name: imagepull-gitea
namespace: xactmon
type: kubernetes.io/dockerconfigjson

238
xactmon/xactmon.yaml Normal file
View File

@ -0,0 +1,238 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: xactmon-receiver-jmap
labels:
app.kubernetes.io/name: xactmon-receiver-jmap
app.kubernetes.io/component: receiver-jmap
spec:
selector:
matchLabels:
app.kubernetes.io/name: xactmon-receiver-jmap
app.kubernetes.io/component: receiver-jmap
template:
metadata:
labels:
app.kubernetes.io/name: xactmon-receiver-jmap
app.kubernetes.io/component: receiver-jmap
spec:
containers:
- name: receiver-jmap
image: git.pyrocufflink.net/packages/xactmon
imagePullPolicy: Always
args:
- receiver-jmap
- /etc/xactmon/config.toml
env:
- name: RUST_LOG
value: xactmon=trace,info
- name: TZ
value: America/Chicago
volumeMounts:
- mountPath: /etc/xactmon
name: xactmon-config
readOnly: true
- mountPath: /run/dch-ca
name: dch-ca
readOnly: true
- mountPath: /run/secrets/xactmon
name: xactmon-secrets
readOnly: true
- mountPath: /run/secrets/rabbitmq/password
name: rabbitmq-cert-password
subPath: password
readOnly: true
- mountPath: /run/secrets/rabbitmq/cert
name: rabbitmq-cert
readOnly: true
- mountPath: /tmp
name: tmp
subPath: tmp
imagePullSecrets:
- name: imagepull-gitea
securityContext:
runAsUser: 251
runAsGroup: 251
fsGroup: 251
volumes:
- name: dch-ca
configMap:
name: dch-root-ca
- name: rabbitmq-cert
secret:
secretName: rabbitmq-cert
defaultMode: 0440
- name: rabbitmq-cert-password
secret:
secretName: rabbitmq-cert-password
defaultMode: 0440
- name: tmp
emptyDir:
medium: Memory
- name: xactmon-config
configMap:
name: xactmon
- name: xactmon-secrets
secret:
secretName: xactmon
defaultMode: 0440
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: xactmon-processor
labels:
app.kubernetes.io/name: xactmon-processor
app.kubernetes.io/component: processor
spec:
selector:
matchLabels:
app.kubernetes.io/name: xactmon-processor
app.kubernetes.io/component: processor
template:
metadata:
labels:
app.kubernetes.io/name: xactmon-processor
app.kubernetes.io/component: processor
spec:
containers:
- name: processor
image: git.pyrocufflink.net/packages/xactmon
imagePullPolicy: Always
args:
- processor
- /etc/xactmon/config.toml
env:
- name: RUST_LOG
value: xactmon=trace,info
- name: TZ
value: America/Chicago
volumeMounts:
- mountPath: /etc/xactmon
name: xactmon-config
readOnly: true
- mountPath: /run/dch-ca
name: dch-ca
readOnly: true
- mountPath: /run/secrets/xactmon
name: xactmon-secrets
readOnly: true
- mountPath: /run/secrets/rabbitmq/password
name: rabbitmq-cert-password
subPath: password
readOnly: true
- mountPath: /run/secrets/rabbitmq/cert
name: rabbitmq-cert
readOnly: true
- mountPath: /tmp
name: tmp
subPath: tmp
imagePullSecrets:
- name: imagepull-gitea
securityContext:
runAsUser: 251
runAsGroup: 251
fsGroup: 251
volumes:
- name: dch-ca
configMap:
name: dch-root-ca
- name: rabbitmq-cert
secret:
secretName: rabbitmq-cert
defaultMode: 0440
- name: rabbitmq-cert-password
secret:
secretName: rabbitmq-cert-password
defaultMode: 0440
- name: tmp
emptyDir:
medium: Memory
- name: xactmon-config
configMap:
name: xactmon
- name: xactmon-secrets
secret:
secretName: xactmon
defaultMode: 0440
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: xactmon-importer-firefly
labels:
app.kubernetes.io/name: xactmon-importer-firefly
app.kubernetes.io/component: importer-firefly
spec:
selector:
matchLabels:
app.kubernetes.io/name: xactmon-importer-firefly
app.kubernetes.io/component: importer-firefly
template:
metadata:
labels:
app.kubernetes.io/name: xactmon-importer-firefly
app.kubernetes.io/component: importer-firefly
spec:
containers:
- name: importer-firefly
image: git.pyrocufflink.net/packages/xactmon
imagePullPolicy: Always
args:
- importer-firefly
- /etc/xactmon/config.toml
env:
- name: RUST_LOG
value: xactmon=trace,info
- name: TZ
value: America/Chicago
volumeMounts:
- mountPath: /etc/xactmon
name: xactmon-config
readOnly: true
- mountPath: /run/dch-ca
name: dch-ca
readOnly: true
- mountPath: /run/secrets/xactmon
name: xactmon-secrets
readOnly: true
- mountPath: /run/secrets/rabbitmq/password
name: rabbitmq-cert-password
subPath: password
readOnly: true
- mountPath: /run/secrets/rabbitmq/cert
name: rabbitmq-cert
readOnly: true
- mountPath: /tmp
name: tmp
subPath: tmp
imagePullSecrets:
- name: imagepull-gitea
securityContext:
runAsUser: 251
runAsGroup: 251
fsGroup: 251
volumes:
- name: dch-ca
configMap:
name: dch-root-ca
- name: rabbitmq-cert
secret:
secretName: rabbitmq-cert
defaultMode: 0440
- name: rabbitmq-cert-password
secret:
secretName: rabbitmq-cert-password
defaultMode: 0440
- name: tmp
emptyDir:
medium: Memory
- name: xactmon-config
configMap:
name: xactmon
- name: xactmon-secrets
secret:
secretName: xactmon
defaultMode: 0440