Squashed commit of the following:

commit 98bff364a9
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Tue May 30 22:58:12 2023 -0500

    add deployment docs

commit cdcae78359
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat May 27 15:24:34 2023 -0500

    origin env

commit 49c01169d3
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat May 27 15:07:49 2023 -0500

    fix build for web-client

commit b4fd08f40f
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 22:33:32 2023 -0500

    update cat endpoint

commit 3edb03e297
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 22:31:01 2023 -0500

    add more configmaps

commit d011b29427
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 22:19:10 2023 -0500

    update configmaps

commit 1049be1bcc
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 21:49:23 2023 -0500

    rearrange

commit b82c24b339
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 21:27:17 2023 -0500

    tweaks

commit 2aa0960b00
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 21:20:49 2023 -0500

    encrypt just the pull secret and merge it in

commit 54261d6089
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 21:13:08 2023 -0500

    try to get kustomization to be happy

commit f09a42837b
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 21:09:41 2023 -0500

    encrypt kustomization.yaml

commit a5dd2c34b0
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 20:51:53 2023 -0500

    add sops and beta specific decryption key

commit 6a88c36646
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 20:18:14 2023 -0500

    attempt to use var replacement

commit 756330e79c
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 19:53:04 2023 -0500

    some lifecycle fixes for app and access control stuff, to get data flowing to the client again

commit 2e0e78ae28
Merge: ebfccca e945fc2
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 19:22:13 2023 -0500

    Merge remote-tracking branch 'origin/feat/mvp' into feat/mvp-deploy-beta

    * origin/feat/mvp:
      pagination and linking improvements
      use 1 table for catalog data + search
      re arrange some things

commit ebfcccabfe
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 19:08:27 2023 -0500

    add prod builds and migrate env to a namespaced configuration in k8s

commit e945fc2f79
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 01:41:44 2023 -0500

    pagination and linking improvements

commit 7970ff99b0
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 26 01:41:17 2023 -0500

    use 1 table for catalog data + search

commit 5221c50814
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Thu May 25 19:48:00 2023 -0500

    re arrange some things

commit 000ff9a711
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Wed May 24 22:57:23 2023 -0500

    cronjob that invokes runner

commit c4cebcea6d
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Wed May 24 18:46:12 2023 -0500

    refactor grpc dial into kernel, trying to figure out runner import slowness

commit d09e5aaf53
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Wed May 24 01:44:04 2023 -0500

    uses k8s to run frontend, has ingress
pull/1/head
Adam Veldhousen 12 months ago
parent e945fc2f79
commit 7abc8666b9
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

4
.gitignore vendored

@ -26,3 +26,7 @@ vite.config.ts.timestamp-*
.DS_Store
# keys
.age.txt

@ -0,0 +1,6 @@
creation_rules:
- path_regex: env/.*/master\.json$
pgp: 4FA79E5B6598505C8DFA30A7A466CEE1415C0B9C
- path_regex: env/base/.*\.yaml$
encrypted_regex: ^(data|stringData)$
age: age1d5vst0g82v6xml29ydsrxefmf3vclgm6dj3npw6mefa7yu9xueaqztjqlg

@ -7,3 +7,5 @@ buf 1.17.0
k9s 0.26.3
golang 1.19
nodejs lts
sops 3.7.3
jq 1.6

@ -15,11 +15,49 @@ clobber:
@rm -f $(KUBECONFIG)
.PHONY: build
build:
docker build --build-arg "service=runner" -t bh/service-runner -f ./src/Dockerfile.service ./src
docker build --build-arg "service=catalog" -t bh/service-catalog -f ./src/Dockerfile.service ./src
docker build --build-arg "service=proxy-admin" -t bh/service-proxy-admin -f ./src/Dockerfile.service ./src
SERVICE = "catalog"
ENV = ""
ORIGIN = "https://barretthousen.com"
BUILD_INITIATOR = "Development Machine"
VERSION = $(shell git rev-parse --verify --short HEAD)
GIT_REF = $(shell git rev-parse --verify HEAD)
BUILD_DATE := $(shell date +%Y-%m-%d-%T)
.PHONY: build-client-image
build-client-image:
docker build --target=production \
--label 'com.barretthousen.service=$(SERVICE)' \
--label 'com.barretthousen.version=$(VERSION)' \
--label 'com.barretthousen.git-ref=$(GIT_REF)' \
--label 'com.barretthousen.build-date=$(BUILD_DATE)' \
--label 'com.barrethousen.builder=$(BUILD_INITIATOR)' \
--build-arg 'origin=$(ORIGIN)' \
-t barretthousen/client-$(SERVICE):$(VERSION) \
-t git.vdhsn.com/barretthousen/client-$(SERVICE):$(VERSION) \
-f ./src/$(SERVICE)/Dockerfile.frontend ./src/$(SERVICE)
@[ ! -z $(ENV) ] && docker tag git.vdhsn.com/barretthousen/client-$(SERVICE):$(VERSION) git.vdhsn.com/barretthousen/client-$(SERVICE):$(ENV) || true
@[ ! -z $(ENV) ] && docker push git.vdhsn.com/barretthousen/client-$(SERVICE):$(VERSION) || true
@[ ! -z $(ENV) ] && docker push git.vdhsn.com/barretthousen/client-$(SERVICE):$(ENV) || true
.PHONY: build-backend-image
build-backend-image:
docker build --target=production \
--label 'com.barretthousen.service=$(SERVICE)' \
--label 'com.barretthousen.version="$(VERSION)"' \
--label 'com.barretthousen.git-ref="$(GIT_REF)"' \
--label 'com.barretthousen.build-date=$(BUILD_DATE)' \
--label 'com.barrethousen.builder=$(BUILD_INITIATOR)' \
--build-arg 'service=$(SERVICE)' \
-t barretthousen/service-$(SERVICE):$(VERSION) \
-t git.vdhsn.com/barretthousen/service-$(SERVICE):$(VERSION) \
-t git.vdhsn.com/barretthousen/service-$(SERVICE):$(ENV) \
-f ./src/Dockerfile.prod-backend ./src
@[ ! -z $(ENV) ] && docker tag git.vdhsn.com/barretthousen/service-$(SERVICE):$(VERSION) git.vdhsn.com/barretthousen/service-$(SERVICE):$(ENV) || true
@[ ! -z $(ENV) ] && docker push git.vdhsn.com/barretthousen/service-$(SERVICE):$(VERSION) || true
@[ ! -z $(ENV) ] && docker push git.vdhsn.com/barretthousen/service-$(SERVICE):$(ENV) || true
.PHONY: acceptance-test
acceptance-test:
@ -36,8 +74,9 @@ gen: $(GOBIN)/sqlc buf.lock
.PHONY: setup
setup: $(GOBIN)/sqlc $(GOBIN)/buf
setup: $(GOBIN)/sqlc $(GOBIN)/buf ./env/.age.txt
@asdf install || true
@docker login git.vdhsn.com
buf.lock: $(GOBIN)/buf
@$(GOBIN)/buf mod update ./src
@ -52,3 +91,11 @@ $(GOBIN)/sqlc:
@KUBECONFIG=$(KUBECONFIG) ctlptl create registry kind-bh-registry --port=5005
@KUBECONFIG=$(KUBECONFIG) ctlptl create cluster kind --name=kind-bh-local --registry=kind-bh-registry --kubernetes-version $(K8S_VERSION)
@kind get kubeconfig --name=bh-local > .kubeconfig
# used to encrypt/decrypt sensitive values with sops
age_identity=$(shell sops -d ./env/master.json)
./env/.age.txt:
@echo "# created: $(shell echo '$(age_identity)' | jq -r '.created')" >> $@
@echo "# public key: $(shell echo '$(age_identity)' | jq -r '.public_key')" >> $@
@echo "$(shell echo '$(age_identity)' | jq -r '.private_key')" >> $@
@echo "$@ created!"

@ -2,6 +2,8 @@
Search and get alerts for items across the most popular auction sites.
Built with microservice architecture, for learning purposes
### Links
- [Keybase Team Chat](keybase://team/barretthousen)
@ -16,7 +18,6 @@ Search and get alerts for items across the most popular auction sites.
1. Ability to search upcoming and live auctions across major auction sites
2. Get an email digest of upcoming auctions for the week
Future goals
1. Get email alerts when these auctions are about to go live
@ -65,9 +66,18 @@ Monolithic postgres datbabase tying it all together
Install `asdf` and run `hack/asdf_plugin_setup`
```sh
make setup # install asdf stuff and build tooling
# install asdf tools, build tooling, encryption key for sops
make setup
# generate protobufs and sql boilerplate
make gen
# spin up a k8s cluster, build and deploy services locally w/ hot reloading
make dev
make gen # generate protobufs and sql boilerplate
# build production docker images for the backend microservices, optionally push to the respective env
make build-backend-image SERVICE=[catalog, runner, proxy-client, proxy-admin] [ENV=[beta, prod]]
make dev # spin up a k8s cluster, build and deploy servicces w/ hot reloading
# build client docker image for web frontends, optionally push to the respective env
make build-client-image SERVICE=[web-client] [ENV=[beta, prod]]
```

@ -16,14 +16,15 @@ helm_repo('grafana', 'https://grafana.github.io/helm-charts', labels=["9-repos"]
helm_resource(
'ingress',
'traefik/traefik',
namespace='barretthousen-local',
flags=[
'--set', 'logs.access.enabled=true'
],
resource_deps=['traefik'],
port_forwards=[
port_forward(8000, 8000, name='Barretthousen'),
port_forward(9000, 9000, name='Traefik', link_path='/dashboard/#/')
],
resource_deps=['traefik'],
labels=["1-ingress"]
)
@ -38,7 +39,7 @@ k8s_resource(
helm_resource(
'postgres',
'bitnami/postgresql',
resource_deps=['bitnami'],
namespace='barretthousen-local',
flags=[
'--set', 'fullnameOverride=bh-db',
'--set', 'auth.enablePostgresUser=true',
@ -47,6 +48,7 @@ helm_resource(
],
port_forwards=[
port_forward(5432, 5432, name='BH DB')],
resource_deps=['bitnami'],
labels=["9-data"])
k8s_yaml(
@ -58,7 +60,8 @@ def bh_backend_service(service="", port_forwards=[], migrateDB=False, devMode=Tr
'{}-go-compile'.format(service),
'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags "all=-N -l" -o .bin/{}-debug ./src/{}'.format(service, service),
deps=['./src/{}'.format(service), './src/lib'],
resource_deps=deps
resource_deps=deps,
labels=['3-compilation']
)
entry_cmd = [
@ -103,7 +106,7 @@ def bh_backend_service(service="", port_forwards=[], migrateDB=False, devMode=Tr
)
k8s_resource(
workload='local-{}'.format(service),
workload='{}-local'.format(service),
port_forwards=port_forwards,
labels=labels,
resource_deps=deps,
@ -130,16 +133,22 @@ bh_backend_service(service="proxy-client", port_forwards=[
docker_build(
ref='barretthousen/service-web-client',
context='./src/web-client',
dockerfile='./src/web-client/Dockerfile.frontend',
dockerfile='./src/web-client/Dockerfile.dev-frontend',
target='development'
)
k8s_resource(
workload='local-web-client',
workload='web-client-local',
port_forwards=['8080:80'],
labels=['2-services'],
resource_deps=['ingress']
)
k8s_resource(
workload='runner-sync-local',
labels='2-services'
)
# local_resource(
# 'web-client',
# dir='./src/web-client',
@ -159,6 +168,7 @@ k8s_resource(
# labels=['2-services']
# )
# helm_resource(
# 'loki-stack',
# 'grafana/loki-stack',

@ -0,0 +1,88 @@
# Deployment
Services deployed (using kustomize):
- [catalog](./src/catalog)
- [runner](./src/runner)
- [proxy-admin](./src/proxy-admin)
- [proxy-client](./src/proxy-client)
- [web-client](./src/web-client)
Platform components deployed (using helm):
- [Traefik]()
- [Postgresql]()
## [Beta](https://beta.barretthousen.com)
The [Beta](https://beta.barretthousen.com) environment lives in my [homelab repo](https://git.vdhsn.com/adam/SunnyHomeLab),
and is auto deployed using Flux w/ kustomize.
See [./env/beta](./env/beta/kustomization.yaml) for how it's set up.
- There are only 3 environments: `local`, `beta`, `prod`
- `local`: optimize for iteration speed, observability, mutability. Ephemeral data. Should be quick to create and destroy.
- `beta`: optimize for likeness with prod, observability, and durable data.
- `prod`: optimize for up time, automated change control, observability, data durability.
- Each environment should pull from the image tag that matches it's name (ie catalog service running in `beta` env will use `git.vdhsn.com/barretthousen/service-catalog:beta` docker image)
- All environments should have resources suffixed with their name (ie `deployment/catalog-beta` in `beta` env)
- All environments must use kustomize, and have the same resources - configuration of those resources can vary as needed
### Initial environment setup
Deploy the following with Helm (assuming we're setting up a new `beta` env with the following commands):
1. Traefik:
```sh
helm repo add traefik https://traefik.github.io/charts;
helm install --upgrade ingress traefik/traefik -n 'barretthousen-beta'\
--set=logs.access.enabled=true
```
2. Postgresql:
```sh
helm repo add bitnami https://charts.bitnami.com/bitnami;
helm install --upgrade bh-db bitnami/postgresql -n 'barretthousen-beta' \
--set='fullnameOverride=bh-db' \
--set='auth.enablePostgresuser=true' \
--set='auth.postgresPassword=bh-admin' \
--set='auth.database=bh'
```
### Deployment steps
1. Build and publish prod images
```sh
make build-backend-image SERVICE=catalog env=beta
make build-backend-image SERVICE=runner env=beta
make build-backend-image SERVICE=proxy-admin env=beta
make build-backend-image SERVICE=proxy-client env=beta
make build-client-image SERVICE=web-client env=beta
```
2. Rolling restart deployments in the beta env
```sh
kubectl rollout restart -n barretthousen-beta deployment runner-beta
kubectl rollout status -n barretthousen-beta deployment runner-beta -w
kubectl rollout restart -n barretthousen-beta deployment catalog-beta
kubectl rollout status -n barretthousen-beta deployment catalog-beta -w
kubectl rollout restart -n barretthousen-beta deployment proxy-admin-beta
kubectl rollout status -n barretthousen-beta deployment proxy-admin-beta -w
kubectl rollout restart -n barretthousen-beta deployment proxy-client-beta
kubectl rollout status -n barretthousen-beta deployment proxy-client-beta -w
kubectl rollout restart -n barretthousen-beta deployment web-client-beta
kubectl rollout status -n barretthousen-beta deployment web-client-beta -w
```
## Prod
TBD

@ -12,9 +12,11 @@ spec:
labels:
service: catalog
spec:
serviceAccountName: barretthousen-service
containers:
- name: catalog
image: barretthousen/service-catalog
image: barretthousen/service-catalog:prod
imagePullPolicy: Always
ports:
- containerPort: 5001
name: grpc

@ -0,0 +1,27 @@
apiVersion: v1
kind: Secret
metadata:
name: bh-registry
type: kubernetes.io/dockerconfigjson
stringData:
.dockerconfigjson: ENC[AES256_GCM,data:bfqlh7Vy3HDYFtgv56xO+8lXOLO9bQWRC16N8hAzv6xJaIN6CmXDwFzoLoGWPrP9s/o446tuOEJEylf5z/ITnLtdGJgMsN13Xk7OiF9B2unV8yOOrzt6U6R2s5cFpbSL3tAHQmDKHxRrzbvyV2J3magen7oHQWbkwkOQq7FqV/k7wFly+bei1u+YLJ9hq798Xa5HG9j4LsVWi5izKt1BBss2xFlo3yzEFqNmQ+AzcUN1uK1xwStplK4IKC36rewONDS+yyqj830LLShb,iv:qDwYxBqK+ZamBcWEuF+UEfW8gLFROagaBqVAc1tCjUI=,tag:OYhChcvisxP0r3kQ4hq4SA==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1d5vst0g82v6xml29ydsrxefmf3vclgm6dj3npw6mefa7yu9xueaqztjqlg
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaRG1ERkdkTXMvUllCSHdS
TXVBVWNMd0hYeXMvTXh6OFVTYXV0MkVoOEJ3Ck9XakJTbHMyTWpvazFzYUtNcmtx
NTVoVnUwWkpKYjg4MWs1dmxpT3JGRFUKLS0tIHdHRk8yL1lCRk9DM0haYjN4Z1Ry
d25rRklvOUdLQlU0S2l0WXBpUXhyR2MKQgJXQgxp0T2rr0V2NjwSjWFlzNyig5vW
S8PW6OpCOyfMqzz5NWTdUVymY7UEdAguwZH+MY2DdvEn3NM/TcnRwA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2023-05-27T02:48:45Z"
mac: ENC[AES256_GCM,data:SCjcJPn7hg9sUFYlOUKAVJBXKNIrcz/x3aqyX43xf7UO7Zo/pGDp1JDaKA7lCaKTgPEAe1zRRv6LjejNGX3DlpmxMS6o2xaI3nb0e0CnLj9t9t57L5svrciwh9wOennWj26DirgzAB+uqCJ/NGOJh4S8yTPOF5MgBNkqNw6FN94=,iv:YTtckdYzKnBBqbQYvjw9FpvGHsUxX6MnAeNopYhFe7I=,tag:BPUitJtY65JbnanHJgJatg==,type:str]
pgp: []
encrypted_regex: ^(data|stringData)$
version: 3.7.3

@ -1,4 +1,6 @@
resources:
- ./image-pull-secret.yaml
- ./namespace.yaml
- ./catalog-deployment.yaml
- ./runner-deployment.yaml
- ./proxy-admin-deployment.yaml
@ -6,3 +8,4 @@ resources:
- ./web-client-deployment.yaml
- ./client-ingress.yaml
- ./admin-ingress.yaml
- ./scrape-cronjob.yaml

@ -0,0 +1,17 @@
apiVersion: v1
kind: Namespace
metadata:
name: barretthousen
annotations:
description: |
environment for barretthousen
labels:
name: barretthousen
app: barretthousen
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: barretthousen-service
imagePullSecrets:
- name: bh-registry

@ -11,9 +11,11 @@ spec:
labels:
service: proxy-admin
spec:
serviceAccountName: barretthousen-service
containers:
- name: proxy-admin
image: barretthousen/service-proxy-admin
image: barretthousen/service-proxy-admin:prod
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
@ -51,4 +53,4 @@ data:
log_level: 2
port: 80
endpoints:
runner: local-runner:5001
runner: runner-local:5001

@ -11,9 +11,11 @@ spec:
labels:
service: proxy-client
spec:
serviceAccountName: barretthousen-service
containers:
- name: proxy-client
image: barretthousen/service-proxy-client
image: barretthousen/service-proxy-client:prod
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
@ -50,5 +52,6 @@ data:
config.yaml: |
log_level: 2
port: 80
access_control_allow_origin: "*"
endpoints:
catalog: local-catalog:5001
catalog: catalog-local:5001

@ -12,11 +12,11 @@ spec:
labels:
service: runner
spec:
serviceAccountName: barretthousen-service
containers:
- name: runner
image: barretthousen/service-runner
tty: true
stdin: true
image: barretthousen/service-runner:prod
imagePullPolicy: Always
ports:
- containerPort: 5001
name: grpc

@ -0,0 +1,24 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: runner-sync
spec:
schedule: "*/2 * * * *"
jobTemplate:
spec:
template:
metadata:
labels:
name: runner-sync
spec:
restartPolicy: OnFailure
containers:
- name: runner-curl
image: curlimages/curl:latest
command:
[
"curl",
"http://proxy-admin-local/api/v1/sync",
'-d=''{"target":"liveauctioneers"}''',
"-vvvv",
]

@ -11,11 +11,16 @@ spec:
labels:
app: web-client
spec:
serviceAccountName: barretthousen-service
containers:
- name: web-client
image: barretthousen/service-web-client
image: barretthousen/service-web-client:prod
imagePullPolicy: Always
stdin: true
tty: true
env:
- name: ORIGIN
value: https://barretthousen.com
ports:
- containerPort: 80
name: http

@ -0,0 +1,22 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: catalog-config
data:
config.yaml: |
log_level: 2
port: 5001
db_service:
scheme: postgres
port: 5432
host: bh-db
name: bh
user: catalog-service
password: catalog-service
db_migrate:
scheme: postgres
port: 5432
host: bh-db
name: bh
user: postgres
password: bh-admin-beta

@ -1,7 +1,67 @@
resources:
- ../base
commonLabels:
environment: beta
nameSuffix: -beta
namespace: barretthousen-beta
patchesStrategicMerge:
- scrape-cronjob.yaml
- catalog-configmap.yaml
- proxy-admin-configmap.yaml
- proxy-client-configmap.yaml
- runner-configmap.yaml
namePrefix: beta-
patches:
- target:
kind: Ingress
name: admin
patch: |-
- op: replace
path: /spec/rules/0/host
value: admin.beta.barretthousen.com
- target:
kind: Ingress
name: web
patch: |-
- op: replace
path: /spec/rules/0/host
value: beta.barretthousen.com
- target:
kind: Deployment
name: catalog
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: git.vdhsn.com/barretthousen/service-catalog:beta
- target:
kind: Deployment
name: runner
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: git.vdhsn.com/barretthousen/service-runner:beta
- target:
kind: Deployment
name: proxy-admin
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: git.vdhsn.com/barretthousen/service-proxy-admin:beta
- target:
kind: Deployment
name: proxy-client
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: git.vdhsn.com/barretthousen/service-proxy-client:beta
- target:
kind: Deployment
name: web-client
patch: |-
- op: replace
path: /spec/template/spec/containers/0/image
value: git.vdhsn.com/barretthousen/client-web-client:beta
- op: replace
path: /spec/template/spec/containers/0/env/0/value
value: https://beta.barretthousen.com

@ -0,0 +1,10 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: proxy-admin-config
data:
config.yaml: |
log_level: 2
port: 80
endpoints:
runner: runner-beta:5001

@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: proxy-client-config
data:
config.yaml: |
log_level: 2
port: 80
access_control_allow_origin: "beta.barretthousen.com"
endpoints:
catalog: catalog-beta:5001

@ -0,0 +1,23 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: runner-config
data:
config.yaml: |
log_level: 2
port: 5001
catalog_endpoint: catalog-beta:5001
db_service:
scheme: postgres
port: 5432
host: bh-db
name: bh
user: runner-service
password: runner-service
db_migrate:
scheme: postgres
port: 5432
host: bh-db
name: bh
user: postgres
password: bh-admin-beta

@ -0,0 +1,19 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: runner-sync
spec:
schedule: "0 */2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: runner-curl
command:
[
"curl",
"http://proxy-admin-beta/api/v1/sync",
'-d=''{"target":"liveauctioneers"}''',
"-vvvv",
]

@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: bh-registry
type: kubernetes.io/dockerconfigjson
stringData:
.dockerconfigjson: |
{ "auths": {} }

@ -4,13 +4,22 @@ resources:
commonLabels:
environment: local
namePrefix: local-
nameSuffix: -local
namespace: barretthousen-local
patchesStrategicMerge:
- debug-catalog.yaml
- debug-runner.yaml
- image-pull-secret.yaml
patches:
- target:
kind: CronJob
name: runner-sync
patch: |-
- op: replace
path: /spec/schedule
value: "* * * * *"
- target:
kind: Ingress
name: admin

23
env/master.json vendored

@ -0,0 +1,23 @@
{
"created": "ENC[AES256_GCM,data:eyA43QPLejsqy/4SSWOkaLPMO2+EbuifxQ==,iv:0wEGpIk4023HaDqmXlCimTC4AviguxqzO8LSCIoBPow=,tag:XmpNZzlxNinyPnWP0U7dXQ==,type:str]",
"public_key": "ENC[AES256_GCM,data:f7DxyKaLbgjHvTmNNa6K/pGqFtxrm/JmTQs+I00YQpr4XP8ja0ff+7vM2qi8hzYWQsZ8wiIr/VsPImi/RPQ=,iv:O23u+cuva4qJZ/OpVEoYr3o5X4GxsPt+U3Q5GgQLymc=,tag:6XJtLb/g+WJLhsFH6M6FlA==,type:str]",
"private_key": "ENC[AES256_GCM,data:TTVQUdE+Xd1M3RHix2bkxggrxo3ILdmonjNcq9Ticb2WSIG+IuR+lytgSb+7UHzjFx8wr/lMfap/v7lAum+nYw08Fd56t0B4aHI=,iv:CmYfrd2MtwoyLxjqWC3TZRdK9CRs96n4BYo51MY6uzs=,tag:GYfVWSYE05/tdRaul4JPxw==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
"azure_kv": null,
"hc_vault": null,
"age": null,
"lastmodified": "2023-05-27T01:53:31Z",
"mac": "ENC[AES256_GCM,data:nUmf5oSS+I7WPoJZ4hCpamhQRYeAeSWnzNM6OMAkar+ZXJ/LMynUNy2BA9hHbX6sNoMh7jrGrABAwfXqoGVCkl0F3PtHGky59uiQ0jmSU48n504dhL8/5kr16MeGSMCuVnp+oVy9V9tYFxt4LTxVzMK9mBr92B7kcRqKsb7jO6w=,iv:A5kjc0qtrTTTDOKBgKLslPwv+InCGNVOWLu1T3LmIYw=,tag:2vhk0BEk6MSZHW8zHrIuQg==,type:str]",
"pgp": [
{
"created_at": "2023-05-27T01:53:31Z",
"enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMAzSNwvjPuwiyAQ/9F9BIr5Qu9VC2y8gIBjNKiDxbR5sOo0gopJnCtAWoM4RU\nZO4aU3+MlUjr25PfYauqmdyc86vzDshbyfsQ8uQJW20pl4wJFzbtsSEgMio89FE5\nlfyKB9WcTvivrVUYSarXE7DrPvdr5qOh5qUrc6HLGaniwyN3MxGnm/qu5Ip2z3i8\n6g2GarUJmWXF1U8F0oXw069ImgvTd1u2gUC+CXMDSW+38FYN5dmtbo271g9/7Ikl\nEaP+7B9PlPUAav8IE/k/dGwqeQOjiEce6h0rxyl8PqgcTvxpaJ8Kd197iTovXeyt\nAK1Fv9sMVBBGi/pma85cPxkn8vU68v6LQJvMSwAJ8y+2rXrUb7nxFt1+iBvJWwRr\napGBhceLriV1eL9l0CtLpZNrQvvldF8mNMaNK0vVGdsDrHZq2wU9jSeKZq92Cy2y\nQ+0sEPlBKJIRTcrghtOgKbNatNWM7zShwmxAJ4Kw6qFSpEOTj9Y4WOL70ivWynSt\np7aaKbSwtwBcXezZZqp1C5/xlcrWal83bsjUqAnXhd30VYBw66JGhZa2PkD0VyqL\n4yoCUC9H8Ea8XtD/z4iG4y8z2yn/+Qa3KoW6vTp78i8OzdzqnfLB9pa6rinueaeV\n1S9y6B+kvwdqDTtgrIfGMUifaE0qE2ZKiPGbyKnqjUrBmY9VUsAQNTkfgXIhQnDS\nXAGDvTBFmmdZzEWE/OP+l1tdk88HJzfhDxIXxdncIYW79ib7bKoqRW6CcrPawxC+\nDn4ykqRZZNYw4j207YXqvYZWBKRCnInWKPmyfT9Ozfd7/HZnX91cKIawwPz2\n=ufAq\n-----END PGP MESSAGE-----\n",
"fp": "A466CEE1415C0B9C"
}
],
"unencrypted_suffix": "_unencrypted",
"version": "3.7.3"
}
}

@ -0,0 +1,2 @@
web-client
.idea

@ -1,17 +1,19 @@
FROM golang:1.19-alpine as builder
FROM golang:1.19 as builder
ARG service
RUN go install github.com/bufbuild/buf/cmd/buf@v1.17.0
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest
COPY . /go/src
WORKDIR /go/src/${service}
WORKDIR /go/src/
RUN /go/bin/sqlc generate -f /go/src/sqlc.yaml
RUN /go/bin/buf mod update /go/src/src \
&& /go/bin/buf generate
RUN /go/bin/buf mod update /go/src && /go/bin/buf generate
ARG service
WORKDIR /go/src/${service}
RUN go mod tidy \
&& CGO_ENABLED=0 go build -v -gcflags="-trimpath=$(go env GOPATH)" -asmflags="-trimpath=$(go env GOPATH)" -o /opt/${service} /go/src/${service}
@ -24,4 +26,4 @@ COPY --from=builder /opt/${service} /opt/${service}
ENV SERVICE=${SERVICE}
ENTRYPOINT ['/opt/${SERVICE}']
ENTRYPOINT ["/opt/${SERVICE}"]

@ -18,6 +18,7 @@ require (
github.com/joho/godotenv v1.5.1 // indirect
github.com/pressly/goose/v3 v3.11.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect

@ -180,6 +180,7 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

@ -5,6 +5,7 @@ go 1.19
require (
github.com/jackc/pgx/v4 v4.18.1
go.uber.org/automaxprocs v1.5.2
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
google.golang.org/grpc v1.55.0
)

@ -175,6 +175,7 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

@ -11,7 +11,6 @@ import (
"github.com/ilyakaznacheev/cleanenv"
_ "go.uber.org/automaxprocs"
"golang.org/x/sync/errgroup"
// "go.uber.org/dig"
)
type App interface {
@ -30,47 +29,48 @@ func Run(parent context.Context, app App) {
ctx, canceller := context.WithCancel(parent)
defer canceller()
sig := make(chan os.Signal, 5)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGHUP)
go func() {
defer canceller()
InfoLog.Println("Starting service 🚀")
select {
case signal := <-sig:
TraceLog.Printf("[SHUTDOWN TRIGGERED] got shutdown signal: %v", signal)
case <-ctx.Done():
TraceLog.Println("[SHUTDOWN TRIGGERED] context exited unexpectedly")
if err := loadConfig(app); err != nil {
ErrorLog.Println(err)
return
}
InfoLog.Println("Shutting down service ⛔⚠️😱")
stopCtx, stopCanceller := context.WithTimeout(parent, time.Second*5)
defer stopCanceller()
if err := app.Start(ctx); err != nil {
ErrorLog.Println(err)
return
}
}()
app.OnStop(stopCtx)
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGHUP)
errs := &errgroup.Group{}
select {
case signal := <-sig:
TraceLog.Printf("[SHUTDOWN TRIGGERED] got shutdown signal: %v", signal)
case <-ctx.Done():
TraceLog.Println("[SHUTDOWN TRIGGERED] context exited")
}
errs.Go(CloseGRPCConns)
errs.Go(StopHTTPServer)
errs.Go(StopGRPCServer)
InfoLog.Println("Shutting down service ⛔⚠️😱")
if err := errs.Wait(); err != nil {
ErrorLog.Printf("There was an error shutting down the application gracefully: %v", err)
}
}()
errs := &errgroup.Group{}
InfoLog.Println("Starting service 🚀")
errs.Go(CloseGRPCConns)
errs.Go(StopHTTPServer)
errs.Go(StopGRPCServer)
if err := loadConfig(app); err != nil {
ErrorLog.Println(err)
return
if err := errs.Wait(); err != nil {
ErrorLog.Printf("There was an error shutting down the application gracefully: %v", err)
}
if err := app.Start(ctx); err != nil {
ErrorLog.Println(err)
return
}
stopCtx, stopCanceller := context.WithTimeout(parent, time.Second*5)
defer stopCanceller()
app.OnStop(stopCtx)
TraceLog.Println("Shutdown process fully completed")
}
func loadConfig(cfg interface{}) error {

@ -51,6 +51,7 @@ func StartGRPCServer(ctx context.Context, port int, sb ServerBuilder, opts ...gr
}
func StopGRPCServer() error {
TraceLog.Println("stopping GRPC server...")
grpcServerInstance.GracefulStop()
return nil
}
@ -83,7 +84,12 @@ func StartHTTPServer(ctx context.Context, port int, handler http.Handler) (err e
}
func StopHTTPServer() error {
ctx, canceler := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5))
if httpServerInstance == nil {
return nil
}
TraceLog.Println("stopping HTTP Server...")
ctx, canceler := context.WithDeadline(context.Background(), time.Now().Add(time.Second*3))
defer canceler()
return httpServerInstance.Shutdown(ctx)
}
@ -132,6 +138,7 @@ func DialGRPC(endpoint string, opts ...grpc.DialOption) (conn *grpc.ClientConn,
}
func CloseGRPCConns() error {
grpcServerInstance.GracefulStop()
grpcConns.Lock()
defer grpcConns.Unlock()
for k, v := range grpcConns.openConns {

@ -27,6 +27,7 @@ require (
go.uber.org/automaxprocs v1.5.2 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect

@ -179,6 +179,8 @@ golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

@ -27,6 +27,7 @@ require (
go.uber.org/automaxprocs v1.5.2 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect

@ -178,6 +178,7 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

@ -15,9 +15,10 @@ import (
)
type ProxyClientApp struct {
LogLevel kernel.LogLevel `yaml:"log_level" env:"BH_LOG_LEVEL" env-default:"0" yaml-default:"0"`
Port int `yaml:"port" env:"PROXY_CLIENT_PORT"`
Endpoints struct {
LogLevel kernel.LogLevel `yaml:"log_level" env:"BH_LOG_LEVEL" env-default:"0" yaml-default:"0"`
Port int `yaml:"port" env:"PROXY_CLIENT_PORT"`
AccessControlAllowOrigin string `yaml:"access_control_allow_origin", env:"PROXY_CLIENT_CORS_ORIGIN`
Endpoints struct {
Catalog string `yaml:"catalog" env:"CATALOG_ENDPOINT"`
} `yaml:"endpoints" env:"PROXY_CLIENT_SERVICES"`
}
@ -45,7 +46,7 @@ func (app *ProxyClientApp) Start(ctx context.Context) error {
Handler: http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
kernel.TraceLog.Printf("{ \"Client\": \"%s\", \"Path\":\"%s\", \"User-Agent\":\"%s\" } ", r.RemoteAddr, r.URL, r.UserAgent())
// TODO: pull the allowed origin host names from the config file
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Origin", app.AccessControlAllowOrigin)
grpcMux.ServeHTTP(w, r)
})),
}

@ -42,7 +42,7 @@ func main() {
flag.Parse()
kernel.Run(context.Background(), &runnerApp{
CatalogEndpoint: "local-catalog:5001",
CatalogEndpoint: "catalog-local:5001",
LogLevel: kernel.LevelTrace,
Port: 5001,
})

@ -0,0 +1,9 @@
FROM node:lts AS development
COPY . /opt/web-client
WORKDIR /opt/web-client
RUN npm install && npm install -g vite
CMD vite dev --port=80 --host=0.0.0.0 --strictPort --logLevel info

@ -1,42 +1,42 @@
FROM node:lts AS development
COPY . /opt/web-client
WORKDIR /opt/web-client
RUN npm install && npm install -g vite
CMD vite dev --port=80 --host=0.0.0.0 --strictPort --logLevel info
FROM node:lts AS build
ARG origin=https://barretthousen.com
ENV ENV=production
ENV PROTOCOL_HEADER=x-forwarded-proto
ENV HOST_HEADER=x-forwarded-host
ENV ADDRESS_HEADER=True-Client-IP
ENV ORIGIN=${origin}
WORKDIR /opt/web-client
COPY --from=development /opt/web-client .
COPY . /opt/web-client
RUN npm build \
&& cp -rv /opt/web-client/package.json /opt/web-client/build \
&& cp -rv /opt/web-client/package-lock.json /opt/web-client/build
RUN npm run build \
&& cp -v /opt/web-client/package.json /opt/web-client/build \
&& cp -v /opt/web-client/package-lock.json /opt/web-client/build \
&& cd /opt/web-client/build && npm ci --omit dev
FROM node:lts AS production
LABEL com.barretthousen.service="web-client"
LABEL com.barretthousen.tags="frontend,public"
LABEL com.barretthousen.release-date="2023-05-01"
LABEL com.barretthousen.version="alpha-0.0.1"
LABEL com.barretthousen.git-ref="000000000000000000000000000000000000000000000000"
ARG origin=https://barretthousen.com
ENV ENV=production
ENV PROTOCOL_HEADER=x-forwarded-proto
ENV HOST_HEADER=x-forwarded-host
ENV ORIGIN=${origin}
WORKDIR /opt
COPY --from=build /var/web-client/build/* /opt/web-client
COPY --from=build /opt/web-client/build/ /opt/web-client/
ENV PORT 80
EXPOSE 80
ENTRYPOINT ['node']
CMD ['web-client']
ENTRYPOINT ["node", "web-client"]

@ -2,6 +2,7 @@
"name": "web-client",
"version": "0.0.1",
"private": true,
"main": "index.js",
"scripts": {
"dev": "vite dev --port 8080",
"build": "vite build",
@ -38,7 +39,6 @@
},
"type": "module",
"dependencies": {
"luxon": "^3.3.0",
"moment": "^2.29.4"
"luxon": "^3.3.0"
}
}

@ -2,15 +2,15 @@ import { browser } from '$app/environment';
import type { PageLoad } from './$types';
// TODO: change to env var
const API_HOST = `${browser ? '': 'http://local-proxy-client.default'}/api/v1`;
const API_HOST = `${browser ? '' : 'http://proxy-client-local'}/api/v1`;
interface SearchPageData {
page: number
limit: number
found: number
total: number
query: string
results: any[]
page: number;
limit: number;
found: number;
total: number;
query: string;
results: any[];
}
export const load = (async ({ fetch, url }): Promise<SearchPageData> => {
@ -21,7 +21,7 @@ export const load = (async ({ fetch, url }): Promise<SearchPageData> => {
try {
// TODO: refactor to one source of truth for all query string building
const response = await fetch(API_HOST + `/upcoming${searchParams.toQueryString()}`);
const { page, total, found, results } = await response.json() || {};
const { page, total, found, results } = (await response.json()) || {};
// TODO: return found results so we can do upperbound on pagination
return {
@ -43,18 +43,17 @@ export const load = (async ({ fetch, url }): Promise<SearchPageData> => {
results: []
};
}
}) satisfies PageLoad;
class SearchParameters {
page: number
limit: number
searchTerm?: string
page: number;
limit: number;
searchTerm?: string;
constructor(url: URL) {
this.searchTerm = url.searchParams.get('query') || undefined;
this.page = Number(url.searchParams.get('page') || 1) - 1;
if (this.page < 0 ) {
if (this.page < 0) {
this.page = 0;
}
@ -79,7 +78,10 @@ class SearchParameters {
}
toQueryString(): string {
const qs = Object.entries(this).filter(t => t.length > 0 && t[1]).map(t => `${t[0]}=${t[1]}`).join('&');
const qs = Object.entries(this)
.filter((t) => t.length > 0 && t[1])
.map((t) => `${t[0]}=${t[1]}`)
.join('&');
return qs === '' ? '' : `?${qs}`;
}
}

Loading…
Cancel
Save