Squashed commit of the following:

commit 30d2938bd6
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat May 6 22:11:09 2023 -0500

    runner service finished

commit 8e33be359a
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat Apr 29 17:25:05 2023 -0500

    add error handling for runner main

commit 118d4ffcc6
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat Apr 29 17:24:28 2023 -0500

    refactor GRPC connection management

commit 852d0f4131
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat Apr 29 17:20:41 2023 -0500

    refactor tiltfile and kustomize for better integration

commit 73fe1eb1d7
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Thu Apr 27 20:31:18 2023 -0500

    use full gobin path for buf in make task

commit 96393815ee
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Thu Apr 20 23:14:17 2023 -0500

    runner runs

commit 18b9523c8b
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Wed Apr 19 21:04:50 2023 -0500

    working on scraping from LA

commit 2699f0c14d
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Wed Apr 19 19:47:51 2023 -0500

    runner starts up and runs migrations
pull/5/head
Adam Veldhousen 1 year ago
parent 6cd624ced1
commit a20b132280
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

5
.gitignore vendored

@ -1,9 +1,10 @@
.kubeconfig
.vscode
# auto generated files
postgres
*.pb.go
*.pb.gw.go
*.swagger.json
src/**/internal/data/postgres/*.go

@ -3,3 +3,6 @@ ctlptl 0.8.18
kind 0.18.0
kustomize 5.0.1
kubectl 1.26.3
buf 1.17.0
k9s 0.26.3
golang 1.19

@ -8,12 +8,13 @@ GOBIN = $(shell go env GOPATH)/bin
dev: .kubeconfig
KUBECONFIG=$(KUBECONFIG) tilt up -f ./src/Tiltfile
.PHONY: clean
clean:
.PHONY: clobber
clobber:
KUBECONFIG=$(KUBECONFIG) ctlptl delete registry kind-bh-registry || true
KUBECONFIG=$(KUBECONFIG) ctlptl delete cluster kind-bh-local || true
@rm -f $(KUBECONFIG)
.PHONY: build
build:
docker build --build-arg "SERVICE=runner" -t bh/service-runner -f ./src/runner/Dockerfile .
@ -29,7 +30,7 @@ acceptance-test:
.PHONY: gen
gen: $(GOBIN)/sqlc buf.lock
@$(GOBIN)/sqlc generate -f ./src/sqlc.yaml
@cd ./src && buf generate
@cd ./src && $(GOBIN)/buf generate
.PHONY: setup
@ -37,7 +38,7 @@ setup: $(GOBIN)/sqlc $(GOBIN)/buf
@asdf install || true
buf.lock: $(GOBIN)/buf
@buf mod update ./src
@$(GOBIN)/buf mod update ./src
$(GOBIN)/buf:
@go install github.com/bufbuild/buf/cmd/buf@v1.17.0

@ -1,2 +1,3 @@
resources:
- ./runner-deployment.yaml
- ./proxy-admin-deployment.yaml

@ -0,0 +1,54 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: proxy-admin
spec:
selector:
matchLabels:
service: proxy-admin
template:
metadata:
labels:
service: proxy-admin
spec:
containers:
- name: proxy-admin
image: barretthousen/service-proxy-admin:latest
ports:
- containerPort: 80
name: http
command:
- /opt/proxy-admin
resources:
limits:
memory: "64Mi"
cpu: "250m"
volumeMounts:
- name: proxy-admin-config
mountPath: /config/
volumes:
- name: proxy-admin-config
configMap:
name: proxy-admin-config
---
apiVersion: v1
kind: Service
metadata:
name: proxy-admin
spec:
selector:
service: proxy-admin
ports:
- port: 80
targetPort: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
name: proxy-admin-config
data:
config.yaml: |
log_level: 2
port: 80
endpoints:
runner: local-runner:5001

@ -6,16 +6,77 @@ spec:
replicas: 1
selector:
matchLabels:
service: bh-runner
service: runner
template:
metadata:
labels:
service: bh-runner
service: runner
spec:
initContainers:
- name: runner-db-migrate
image: barretthousen/service-runner:latest
command:
- /opt/runner
args:
- -migrate
resources:
limits:
cpu: "250m"
memory: "64Mi"
volumeMounts:
- mountPath: /config/
name: runner-config
containers:
- name: runner
image: barretthousen/service-runner:latest
ports:
- containerPort: 5001
name: grpc
command:
- /opt/runner
resources:
limits:
cpu: "250m"
memory: "128Mi"
volumeMounts:
- mountPath: /config/
name: runner-config
volumes:
- name: runner-config
configMap:
name: runner-config
---
apiVersion: v1
kind: Service
metadata:
name: runner
spec:
selector:
service: runner
ports:
- port: 5001
targetPort: 5001
---
apiVersion: v1
kind: ConfigMap
metadata:
name: runner-config
data:
config.yaml: |
log_level: 2
port: 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

@ -0,0 +1,23 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: runner
spec:
template:
spec:
containers:
- name: runner
command:
- /go/bin/dlv
args:
- --headless
- --listen=0.0.0.0:2345
- --api-version=2
- --log
- --accept-multiclient
#- --log-output=rpc,dap
- exec
- /opt/runner-debug
- --continue
ports:
- containerPort: 2345

@ -5,3 +5,6 @@ commonLabels:
environment: local
namePrefix: local-
patchesStrategicMerge:
- debug-runner.yaml

@ -3,4 +3,5 @@ go 1.19
use (
./src/lib
./src/runner
./src/proxy-admin
)

@ -0,0 +1,6 @@
go.uber.org/dig v1.0.0/go.mod h1:z+dSd2TP9Usi48jL8M3v63iSBVkiwtVyMKxMZYYauPg=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=

@ -0,0 +1,6 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="bh-db@localhost" uuid="05aba6c0-168c-47e3-b807-951316a3a483">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/src.iml" filepath="$PROJECT_DIR$/.idea/src.iml" />
</modules>
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="DBE_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

@ -1,20 +1,31 @@
FROM golang:1.19-alpine as builder
FROM golang:1.19-alpine as development
ARG SERVICE
ENV SERVICE=${SERVICE}
RUN go install github.com/go-delve/delve/cmd/dlv@latest
COPY . /go/src
WORKDIR /go/src/${SERVICE}
RUN go mod tidy && go build -v -o /opt/${SERVICE} /go/src/${SERVICE}
RUN go mod tidy \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -o /opt/${SERVICE} /go/src/${SERVICE} \
&& CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -gcflags="-N -l" -v -o /opt/${SERVICE}-debug /go/src/${SERVICE}
# && go build -v -gcflags="all=-N -l" -o /opt/${SERVICE}-debug /go/src/${SERVICE}
ENTRYPOINT ['/go/bin/dlv']
CMD ['debug', '/go/src/${SERVICE}']
FROM alpine
FROM alpine as production
ARG SERVICE
ENV SERVICE=${SERVICE}
COPY --from=builder /opt/${SERVICE} /opt/${SERVICE}
COPY --from=development /opt/${SERVICE} /opt/${SERVICE}
CMD /opt/${SERVICE}
ENTRYPOINT ['/opt/${SERVICE}']

@ -0,0 +1,9 @@
FROM golang:1.19-alpine as builder
ARG SERVICE
COPY . /go/src
WORKDIR /go/src/${SERVICE}
RUN go mod tidy && go build -v -o /opt/${SERVICE} /go/src/${SERVICE}

@ -23,12 +23,12 @@ helm_resource(
labels=["1-ingress"]
)
helm_resource(
'postgres',
'bitnami/postgresql',
resource_deps=['bitnami'],
flags=[
'--set', 'fullnameOverride=bh-db',
'--set', 'auth.enablePostgresUser=true',
'--set', 'auth.postgresPassword=bh-admin',
'--set', 'auth.database=bh',
@ -36,39 +36,49 @@ helm_resource(
port_forwards=["5432:5432"],
labels=["9-data"])
helm_resource(
'loki-stack',
'grafana/loki-stack',
resource_deps=['grafana'],
flags=[
'--set', 'fluent-bit.enabled=false',
'--set', 'promtail.enabled=false',
'--set', 'loki.enabled=false',
],
port_forwards=["3000:80"],
labels=["9-monitoring"])
def bh_service(service=""):
def bh_service(service="", port_forwards=[], devMode=True, labels=['2-services']):
docker_build(
ref="barretthousen/service-{}".format(service),
dockerfile="./Dockerfile.service",
context=".",
target="development" if devMode else "production",
build_args={
"SERVICE": service
},
only=[
"{}".format(service),
"lib",
"Dockerfile.service"
]
# only=[
# "./{}".format(service),
# "lib",
# "Dockerfile.service"
# ]
)
k8s_resource(
workload='local-{}'.format(service),
port_forwards=port_forwards,
labels=labels,
resource_deps=['postgres']
)
bh_service(service="runner")
bh_service(service="runner", port_forwards=[5001, 2345])
bh_service(service="proxy-admin", port_forwards=["8082:80"])
k8s_yaml(
kustomize("../env/local")
)
# helm_resource(
# 'loki-stack',
# 'grafana/loki-stack',
# resource_deps=['grafana'],
# flags=[
# '--set', 'fluent-bit.enabled=false',
# '--set', 'promtail.enabled=false',
# '--set', 'loki.enabled=false',
# ],
# port_forwards=["3000:80"],
# labels=["9-monitoring"])

@ -4,8 +4,10 @@ deps:
- remote: buf.build
owner: googleapis
repository: googleapis
commit: 5ae7f88519b04fe1965da0f8a375a088
commit: cc916c31859748a68fd229a3c8d7a2e8
digest: shake256:469b049d0eb04203d5272062636c078decefc96fec69739159c25d85349c50c34c7706918a8b216c5c27f76939df48452148cff8c5c3ae77fa6ba5c25c1b8bf8
- remote: buf.build
owner: grpc-ecosystem
repository: grpc-gateway
commit: a1ecdc58eccd49aa8bea2a7a9022dc27
digest: shake256:efdd86fbdc42e8b7259fe461a49656827a03fb7cba0b3b9eb622ca10654ec6beccb9a051229c1553ccd89ed3e95d69ad4d7c799f1da3f3f1bd447b7947a4893e

@ -0,0 +1,16 @@
package catalog
import "time"
type Auction struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
SourceSiteURL string `json:"source_site_url,omitempty"`
SourceSiteName string `json:"source_site_name,omitempty"`
SourceURL string `json:"source_url,omitempty"`
Country string `json:"country,omitempty"`
Province string `json:"province,omitempty"`
ItemCount int `json:"itemCount,omitempty"`
Start time.Time `json:"start,omitempty"`
End time.Time `json:"end,omitempty"`
}

@ -0,0 +1,136 @@
package runner
import (
"context"
"fmt"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/domain/catalog"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
"golang.org/x/sync/errgroup"
)
var targetsImpls = map[string]UpcomingAuctionFinder{}
type UpcomingAuctionFinder interface {
fmt.Stringer
Find(ctx context.Context, limit int, results chan<- catalog.Auction) error
}
func RegisterAuctionFinder(finder UpcomingAuctionFinder) {
targetName := finder.String()
if _, ok := targetsImpls[targetName]; ok {
kernel.FatalErr(fmt.Errorf("target %q has already been registered", targetName))
}
targetsImpls[targetName] = finder
}
type (
Domain struct {
Storage
CatalogService
}
CompleteScrapeJobStatus struct {
AuctionCount int
Errors string
}
Storage interface {
CreateScrapeJob(context.Context, string) (ScrapeJob, error)
CompleteScrapeJob(context.Context, int, CompleteScrapeJobStatus) (ScrapeJob, error)
GetJobs(context.Context) ([]ScrapeJob, error)
}
CatalogService interface {
UpdateUpcomingAuction(catalog.Auction) error
}
)
type CatalogServiceStub struct{}
func (css *CatalogServiceStub) UpdateUpcomingAuction(a catalog.Auction) error {
kernel.TraceLog.Printf("Invoke CatalogService[UpdateUpcomingAuction](%+v)", a)
return nil
}
type (
FindNewUpcomingInput struct {
TargetSite string
}
FindNewUpcomingOutput struct {
Job ScrapeJob
}
)
func (domain Domain) FindNewUpcoming(ctx context.Context, in FindNewUpcomingInput) (out FindNewUpcomingOutput, err error) {
for k := range targetsImpls {
kernel.TraceLog.Printf("Find Target: %q", k)
}
finder, ok := targetsImpls[in.TargetSite]
if !ok {
err = fmt.Errorf("could not find target matching name")
return
}
if out.Job, err = domain.Storage.CreateScrapeJob(ctx, in.TargetSite); err != nil {
err = fmt.Errorf("could not create new scrape job record: %w", err)
return
}
kernel.InfoLog.Printf("Scrape Job %d starting", out.Job.ID)
found := make(chan catalog.Auction)
errGroup, innerCtx := errgroup.WithContext(ctx)
errGroup.Go(func() error {
return finder.Find(innerCtx, 0, found)
})
count := 0
for auction := range found {
count++
a := auction
errGroup.Go(func() error {
return domain.CatalogService.UpdateUpcomingAuction(a)
})
}
errMsg := ""
if err = errGroup.Wait(); err != nil {
err = fmt.Errorf("an issue occurred while finding upcoming items iteration: %w", err)
errMsg = err.Error()
}
if out.Job, err = domain.Storage.CompleteScrapeJob(ctx, out.Job.ID, CompleteScrapeJobStatus{
AuctionCount: count,
Errors: errMsg,
}); err != nil {
err = fmt.Errorf("Could not complete scrape job, failing: %w", err)
return
}
kernel.InfoLog.Printf("Scrape Job %d completed in %v.", out.Job.ID, out.Job.Completed.Sub(out.Job.Started))
return
}
type (
GetJobsInput struct{}
GetJobsOutput struct {
Jobs []ScrapeJob
}
)
func (domain Domain) GetJobs(ctx context.Context, in GetJobsInput) (out GetJobsOutput, err error) {
scrapeJobs, err := domain.Storage.GetJobs(ctx)
if err != nil {
err = fmt.Errorf("could not fetch jobs from storage: %w", err)
return
}
out = GetJobsOutput{Jobs: scrapeJobs}
return
}

@ -0,0 +1,12 @@
package runner
import "time"
type ScrapeJob struct {
ID int `json:"id,omitempty"`
Started time.Time `json:"startedTs,omitempty"`
Completed time.Time `json:"completedTs,omitempty"`
TargetSite string `json:"target_site,omitempty"`
AuctionsFound int `json:"auctions_found,omitempty"`
Errors string `json:"errors,omitempty"`
}

@ -5,9 +5,18 @@ go 1.19
require (
github.com/jackc/pgx/v4 v4.18.1
go.uber.org/automaxprocs v1.5.2
go.uber.org/dig v1.16.1
)
require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/joho/godotenv v1.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
)
require (
github.com/ilyakaznacheev/cleanenv v1.4.2
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect

@ -1,4 +1,6 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@ -11,6 +13,8 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/ilyakaznacheev/cleanenv v1.4.2 h1:nRqiriLMAC7tz7GzjzUTBHfzdzw6SQ7XvTagkFqe/zU=
github.com/ilyakaznacheev/cleanenv v1.4.2/go.mod h1:i0owW+HDxeGKE0/JPREJOdSCPIyOnmh6C0xhWAkF/xA=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
@ -60,6 +64,8 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -108,6 +114,8 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/automaxprocs v1.5.2 h1:2LxUOGiR3O6tw8ui5sZa2LAaHnsviZdVOUZw4fvbnME=
go.uber.org/automaxprocs v1.5.2/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8=
go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@ -188,5 +196,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=

@ -2,26 +2,30 @@ package kernel
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/ilyakaznacheev/cleanenv"
_ "go.uber.org/automaxprocs"
// "go.uber.org/dig"
)
type App interface {
Start(context.Context) error
OnStop(context.Context)
GetLogLevel() LogLevel
}
type AppConfig struct {
App
LogLevel LogLevel
LogLevel LogLevel `yaml:"log_level" env:"BH_LOG_LEVEL" env-default:"ERROR" yaml-default:"ERROR"`
Config interface{} `yaml:"service" env:"BH_SERVICE"`
}
func Run(parent context.Context, cfg AppConfig) {
SetLogLevel(cfg.LogLevel)
func Run(parent context.Context, app App) {
SetLogLevel(app.GetLogLevel())
ctx, canceller := context.WithCancel(parent)
defer canceller()
@ -32,18 +36,45 @@ func Run(parent context.Context, cfg AppConfig) {
defer canceller()
select {
case <-sig:
case signal := <-sig:
TraceLog.Printf("[SHUTDOWN TRIGGERED] got shutdown signal: %v", signal)
case <-ctx.Done():
TraceLog.Println("[SHUTDOWN TRIGGERED] context exited unexpectedly")
}
InfoLog.Println("Shutting down service")
InfoLog.Println("Shutting down service ⛔⚠️😱")
stopCtx, stopCanceller := context.WithTimeout(parent, time.Second*5)
defer stopCanceller()
cfg.App.OnStop(stopCtx)
app.OnStop(stopCtx)
}()
InfoLog.Println("Starting service")
if err := cfg.App.Start(ctx); err != nil {
FatalErr(err)
InfoLog.Println("Starting service 🚀")
if err := loadConfig(app); err != nil {
ErrorLog.Println(err)
return
}
if err := app.Start(ctx); err != nil {
ErrorLog.Println(err)
return
}
}
func loadConfig(cfg interface{}) error {
fp := os.Getenv("BH_APP_CONFIG_PATH")
if fp == "" {
fp = "/config/config.yaml"
}
if err := cleanenv.ReadConfig(fp, cfg); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("could not read config from file: %w", err)
}
if err := cleanenv.ReadEnv(cfg); err != nil {
return fmt.Errorf("could not read config from env: %w", err)
}
return nil
}

@ -0,0 +1,86 @@
package kernel
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"google.golang.org/grpc"
)
type ServerBuilder func(grpc.ServiceRegistrar, string)
var (
grpcServerInstance *grpc.Server
httpServerInstance *http.Server
listener net.Listener
)
func StartGRPCServer(ctx context.Context, port int, sb ServerBuilder, opts ...grpc.ServerOption) (endpoint string, err error) {
if grpcServerInstance != nil {
err = errors.New("There can only be one GRPC server running at a time")
return
}
endpoint = fmt.Sprintf("0.0.0.0:%d", port)
listener, err = net.Listen("tcp", endpoint)
if err != nil {
endpoint = ""
err = fmt.Errorf("could not start tcp listener: %w", err)
return
}
grpcServerInstance = grpc.NewServer(opts...)
sb(grpcServerInstance, endpoint)
if err = grpcServerInstance.Serve(listener); err != nil {
endpoint = ""
err = fmt.Errorf("could not serve GRPC server over listener: %w", err)
return
}
InfoLog.Printf("Listening for GRPC requests on %s", endpoint)
return
}
func StopGRPCServer() error {
grpcServerInstance.GracefulStop()
return nil
}
func StartHTTPServer(ctx context.Context, port int, handler http.Handler) (err error) {
if httpServerInstance != nil {
err = errors.New("There can only be one HTTP server running at a time")
return
}
endpoint := fmt.Sprintf("0.0.0.0:%d", port)
listener, err = net.Listen("tcp", endpoint)
if err != nil {
err = fmt.Errorf("could not start tcp listener: %w", err)
return
}
httpServerInstance = &http.Server{
Handler: handler,
Addr: endpoint,
ReadHeaderTimeout: time.Second * 1,
}
if err = httpServerInstance.Serve(listener); err != nil {
err = fmt.Errorf("could not serve http over listener: %w", err)
}
InfoLog.Printf("Listening for HTTP requests on %s", endpoint)
return
}
func StopHTTPServer() error {
ctx, canceler := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5))
defer canceler()
return httpServerInstance.Shutdown(ctx)
}

@ -62,11 +62,13 @@ type Logger interface {
func SetLogLevel(ll LogLevel) {
for i := 0; i < len(loggers); i++ {
lg := loggers[i]
if i > int(ll) {
lg.SetOutput(writer)
} else {
lg.SetOutput(os.Stdout)
target := writer
if int(ll) >= i {
target = os.Stdout
}
loggers[i].SetOutput(target)
}
}

@ -25,7 +25,7 @@ func (pc PostgresConnection) String() string {
}
func NewDBConnection(ctx context.Context, pg PostgresConnection) (conn *pgx.Conn, err error) {
for retries := 0; retries < 3; retries++ {
for retries := 0; retries < 5; retries++ {
if conn, err = pgx.Connect(ctx, pg.String()); err != nil {
sleepTime := time.Second * time.Duration(retries)
log.Printf("%d attempt(s) to postgres failed, retrying in %v: %v", retries+1, sleepTime, err)
@ -42,3 +42,18 @@ func NewDBConnection(ctx context.Context, pg PostgresConnection) (conn *pgx.Conn
return conn, nil
}
func Migrate(ctx context.Context, pg PostgresConnection, sql string) error {
conn, err := NewDBConnection(ctx, pg)
if err != nil {
return err
}
defer conn.PgConn().Close(ctx)
if _, err := conn.Exec(ctx, sql); err != nil {
return err
}
return nil
}

@ -0,0 +1,11 @@
module git.vdhsn.com/barretthousen/barretthousen/src/proxy-admin
go 1.19
require (
git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0
git.vdhsn.com/barretthousen/barretthousen/src/runner v1.0.0
)
replace git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0 => ../lib
replace git.vdhsn.com/barretthousen/barretthousen/src/runner v1.0.0 => ../runner

@ -0,0 +1,59 @@
package main
import (
"context"
"fmt"
"net/http"
"time"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
"git.vdhsn.com/barretthousen/barretthousen/src/runner/api"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type ProxyAdminApp struct {
LogLevel kernel.LogLevel `yaml:"log_level" env:"BH_LOG_LEVEL" env-default:"0" yaml-default:"0"`
Port int `yaml:"port" env:"PROXY_ADMIN_PORT"`
Endpoints struct {
Runner string `yaml:"runner" env:"RUNNER_ENDPOINT"`
} `yaml:"endpoints" env:"PROXY_ADMIN_SERVICES"`
}
func (app *ProxyAdminApp) Start(ctx context.Context) error {
grpcMux := runtime.NewServeMux()
err := api.RegisterRunnerHandlerFromEndpoint(ctx, grpcMux, app.Endpoints.Runner, []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
})
if err != nil {
return err
}
kernel.TraceLog.Printf("%+v", app)
httpServer := &http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", app.Port),
ReadHeaderTimeout: time.Second,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
kernel.TraceLog.Printf("{ \"Client\": \"%s\", \"Path\":\"%s\"} ", r.RemoteAddr, r.URL.Path)
grpcMux.ServeHTTP(w, r)
}),
}
kernel.InfoLog.Printf("Starting HTTP proxy @ %q", httpServer.Addr)
return httpServer.ListenAndServe()
}
func (app *ProxyAdminApp) OnStop(ctx context.Context) {
}
func (app *ProxyAdminApp) GetLogLevel() kernel.LogLevel { return app.LogLevel }
func main() {
kernel.Run(context.Background(), &ProxyAdminApp{
LogLevel: kernel.LevelTrace,
Port: 80,
})
}

@ -5,9 +5,9 @@ package main;
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option go_package = "git.vdhsn.com/barretthousen/barretthousen/src/lib/services/runner";
option go_package = "git.vdhsn.com/barretthousen/barretthousen/src/runner/api";
service Accounts {
service Runner {
rpc FindNewUpcoming(FindNewUpcomingCommand) returns (JobResult) {
option (google.api.http) = {
put: "/v1/findnewupcoming"

@ -0,0 +1,3 @@
log_level: ERROR
service: {}

@ -2,20 +2,38 @@ module git.vdhsn.com/barretthousen/barretthousen/src/runner
go 1.19
require git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0
require (
git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923
google.golang.org/protobuf v1.28.1
)
require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/ilyakaznacheev/cleanenv v1.4.2 // indirect
github.com/joho/godotenv v1.4.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
)
require (
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.0 // indirect
github.com/jackc/pgconn v1.14.0
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect
github.com/jackc/pgx/v4 v4.18.1
go.uber.org/automaxprocs v1.5.2 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/text v0.8.0 // indirect
google.golang.org/grpc v1.54.0
)
replace git.vdhsn.com/barretthousen/barretthousen/src/lib v1.0.0 => ../lib

@ -1,4 +1,6 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
@ -13,7 +15,17 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08=
github.com/ilyakaznacheev/cleanenv v1.4.2 h1:nRqiriLMAC7tz7GzjzUTBHfzdzw6SQ7XvTagkFqe/zU=
github.com/ilyakaznacheev/cleanenv v1.4.2/go.mod h1:i0owW+HDxeGKE0/JPREJOdSCPIyOnmh6C0xhWAkF/xA=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@ -62,12 +74,16 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -146,7 +162,10 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
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=
@ -163,6 +182,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -173,8 +194,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@ -189,8 +211,18 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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 v0.0.0-20230223222841-637eb2293923 h1:znp6mq/drrY+6khTAlJUDNFFcDGV2ENLYKpMq8SyCds=
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
@ -199,3 +231,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw=

@ -0,0 +1,33 @@
-- name: GetJobs :many
SELECT id,
startedTs,
completedTs,
targetSiteName,
auctionsFound,
errors
FROM runner.scrapejob;
-- name: GetJobByID :one
SELECT id,
startedTs,
completedTs,
targetSiteName,
auctionsFound,
errors
FROM runner.scrapejob
WHERE id = $1;
-- name: CreateScrapeJob :one
INSERT INTO runner.scrapejob (
targetSiteName
) VALUES (
$1
) RETURNING *;
-- name: CompleteScrapeJob :exec
UPDATE runner.scrapejob SET
completedTs = $2,
auctionsFound = $3,
errors = $4
WHERE id = $1;

@ -0,0 +1,31 @@
START TRANSACTION;
CREATE SCHEMA IF NOT EXISTS runner;
CREATE TABLE IF NOT EXISTS runner.scrapejob (
id SERIAL PRIMARY KEY,
startedTs TIMESTAMP NOT NULL DEFAULT NOW(),
completedTs TIMESTAMP,
targetSiteName VARCHAR(512) NOT NULL,
auctionsFound INT NOT NULL DEFAULT 0,
errors VARCHAR(5000) NOT NULL DEFAULT ''
);
DO
$do$
BEGIN
IF NOT EXISTS (
SELECT FROM pg_catalog.pg_roles -- SELECT list can be empty for this
WHERE rolname = 'runner-service') THEN
CREATE USER "runner-service" WITH PASSWORD 'runner-service';
END IF;
END
$do$;
GRANT CONNECT ON DATABASE bh to "runner-service";
GRANT USAGE ON SCHEMA runner TO "runner-service";
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA runner TO "runner-service";
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA runner TO "runner-service";
COMMIT;

@ -0,0 +1,82 @@
package data
import (
"context"
"database/sql"
"fmt"
"time"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/domain/runner"
"git.vdhsn.com/barretthousen/barretthousen/src/runner/internal/data/postgres"
)
type PGRunnerStorage struct {
*postgres.Queries
}
func (db *PGRunnerStorage) CreateScrapeJob(ctx context.Context, target string) (sj runner.ScrapeJob, err error) {
rsj, err := db.Queries.CreateScrapeJob(ctx, target)
if err != nil {
return runner.ScrapeJob{}, err
}
return runner.ScrapeJob{
ID: int(rsj.ID),
Started: rsj.Startedts,
TargetSite: rsj.Targetsitename,
}, nil
}
func (db *PGRunnerStorage) CompleteScrapeJob(ctx context.Context, ID int, status runner.CompleteScrapeJobStatus) (sj runner.ScrapeJob, err error) {
completedTime := time.Now().UTC()
if err = db.Queries.CompleteScrapeJob(ctx, postgres.CompleteScrapeJobParams{
ID: int32(ID),
Completedts: sql.NullTime{
Time: completedTime,
Valid: true,
},
Auctionsfound: int32(status.AuctionCount),
Errors: status.Errors,
}); err != nil {
err = fmt.Errorf("could update scrape job in DB: %w", err)
return
}
var rsj postgres.RunnerScrapejob
if rsj, err = db.Queries.GetJobByID(ctx, int32(ID)); err != nil {
err = fmt.Errorf("could not get job by ID: %w", err)
return
}
sj = runner.ScrapeJob{
ID: ID,
Started: rsj.Startedts,
Completed: completedTime,
TargetSite: rsj.Targetsitename,
AuctionsFound: status.AuctionCount,
Errors: status.Errors,
}
return
}
func (db *PGRunnerStorage) GetJobs(ctx context.Context) (results []runner.ScrapeJob, err error) {
var jobs []postgres.RunnerScrapejob
if jobs, err = db.Queries.GetJobs(ctx); err != nil {
err = fmt.Errorf("Couldn't get jobs from DB: %w", err)
return
}
for _, j := range jobs {
results = append(results, runner.ScrapeJob{
ID: int(j.ID),
Started: j.Startedts,
Completed: j.Completedts.Time,
TargetSite: j.Targetsitename,
AuctionsFound: int(j.Auctionsfound),
Errors: j.Errors,
})
}
return
}

@ -0,0 +1,215 @@
package domain
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/domain/catalog"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/domain/runner"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
)
func init() {
kernel.TraceLog.Println("Registering AuctionFinder liveauctioneers")
runner.RegisterAuctionFinder(
LAAuctionFinder("liveauctioneers"),
)
}
type LAAuctionFinder string
func (la LAAuctionFinder) String() string {
return string(la)
}
func (la LAAuctionFinder) Find(ctx context.Context, limit int, results chan<- catalog.Auction) (err error) {
defer close(results)
fetched := -1
total := 0
page := uint(0)
for fetched < limit {
var ids LACatalogIDs
if ids, total, err = LAGetUpcomingSaleIDs(ctx, GetUpcomingSaleIDsInput{
Page: page,
Limit: 128,
}); err != nil {
return err
}
if limit <= 0 {
limit = total
}
var auctions []catalog.Auction
if auctions, err = LAGetSaleInfo(ctx, ids); err != nil {
return err
}
fetched += len(auctions)
for _, a := range auctions {
results <- a
}
if fetched >= limit {
break
}
page++
}
return
}
type GetUpcomingSaleIDsInput struct {
Page uint
Limit uint
}
func LAGetUpcomingSaleIDs(ctx context.Context, in GetUpcomingSaleIDsInput) (ids LACatalogIDs, total int, err error) {
if in.Limit == 0 {
in.Limit = 128
}
req, _ := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf(
"https://search-party-prod.liveauctioneers.com/search/catalogsearch?c=20170802&client=web&client_version=5.0.0&excludedHouses=[]&max_facet_values=0&offset=%d&sort=saleStart&pageSize=%d",
in.Page,
in.Limit,
),
nil)
req.Header.Set("User-Agent", "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0")
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Encoding", "gzip")
req.Header.Set("Cache-Control", "no-cache")
var res *http.Response
if res, err = http.DefaultClient.Do(req); err != nil {
return
}
if res.StatusCode > 299 {
err = fmt.Errorf("Got a bad http status: %d", res.StatusCode)
return
}
defer res.Body.Close()
var searchResults struct {
Payload struct {
Count int `json:"count"`
Results []struct {
ID int `json:"catid"`
} `json:"results"`
} `json:"payload"`
}
if err = json.NewDecoder(res.Body).Decode(&searchResults); err != nil {
return
}
if searchResults.Payload.Results == nil {
return
}
total = searchResults.Payload.Count
ids = LACatalogIDs(make([]int, len(searchResults.Payload.Results)))
for idx, result := range searchResults.Payload.Results {
ids[idx] = result.ID
}
return
}
type LACatalogIDs []int
func (cip LACatalogIDs) String() string {
sb := &strings.Builder{}
sb.WriteString("{\"ids\":[")
for idx, catID := range cip {
fmt.Fprintf(sb, "%d", catID)
if idx < len(cip)-1 {
sb.WriteString(",")
}
}
sb.WriteString("]}")
return sb.String()
}
func LAGetSaleInfo(ctx context.Context, catIDs LACatalogIDs) (results []catalog.Auction, err error) {
if catIDs == nil || len(catIDs) == 0 {
return
}
req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
"https://item-api-prod.liveauctioneers.com/spa/small/catalogs",
strings.NewReader(catIDs.String()))
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0")
req.Header.Set("Content-Type", "application/json")
var res *http.Response
if res, err = http.DefaultClient.Do(req); err != nil {
err = fmt.Errorf("could not complete request: %w", err)
return
}
if res.StatusCode >= 300 {
err = fmt.Errorf("got non 200 status: %d", res.StatusCode)
return
}
defer res.Body.Close()
var apiResults struct {
Data struct {
Catalogs []struct {
ID int `json:"catid"`
Title string `json:"title"`
Description string `json:"description"`
ItemCount int `json:"lotsListed"`
SaleStartTS int64 `json:"saleStartTs"`
Address struct {
CountryCode string `json:"country"`
Lat float64 `json:"lat"`
Long float64 `json:"lng"`
State string `json:"state"`
City string `json:"city"`
} `json:"address"`
} `json:"catalogs"`
} `json:"data"`
}
if err = json.NewDecoder(res.Body).Decode(&apiResults); err != nil {
err = fmt.Errorf("could not parse response: %w", err)
return
}
results = make([]catalog.Auction, len(apiResults.Data.Catalogs))
for idx, c := range apiResults.Data.Catalogs {
results[idx] = catalog.Auction{
Title: c.Title,
Description: c.Description,
SourceSiteURL: "https://www.liveauctioneers.com",
SourceSiteName: "Live Auctioneers",
SourceURL: fmt.Sprintf("https://www.liveauctioneers.com/catalog/%d", c.ID),
Start: time.Unix(c.SaleStartTS, 0),
End: time.Unix(c.SaleStartTS, 0).Add(time.Hour * 8),
ItemCount: c.ItemCount,
Country: c.Address.CountryCode,
Province: c.Address.City,
}
}
return
}

@ -0,0 +1,30 @@
package domain
import "testing"
func Test_catIDPayload_String(t *testing.T) {
tests := []struct {
name string
cip LACatalogIDs
want string
}{
{
name: "happy path",
cip: LACatalogIDs{283257, 283882, 284163},
want: "{\"ids\":[283257,283882,284163]}",
},
{
name: "no items",
cip: LACatalogIDs{},
want: "{\"ids\":[]}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.cip.String(); got != tt.want {
t.Errorf("catIDPayload.String() = %v, want %v", got, tt.want)
}
})
}
}

@ -0,0 +1,65 @@
package internal
import (
"context"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/domain/runner"
"git.vdhsn.com/barretthousen/barretthousen/src/runner/api"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
)
func NewRunnerServer(d *runner.Domain) func(grpcServer grpc.ServiceRegistrar, endpoint string) {
return func(grpcServer grpc.ServiceRegistrar, endpoint string) {
api.RegisterRunnerServer(grpcServer, &runnerHandler{domain: d})
}
}
type runnerHandler struct {
api.UnimplementedRunnerServer
domain *runner.Domain
}
func (rh *runnerHandler) FindNewUpcoming(ctx context.Context, cmd *api.FindNewUpcomingCommand) (*api.JobResult, error) {
out, err := rh.domain.FindNewUpcoming(ctx, runner.FindNewUpcomingInput{
TargetSite: cmd.TargetSite,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "method FindNewUpcoming failed: %q", err.Error())
}
return &api.JobResult{
Id: int32(out.Job.ID),
AuctionsFound: int32(out.Job.AuctionsFound),
CreatedTs: timestamppb.New(out.Job.Started),
CompletedTs: timestamppb.New(out.Job.Completed),
TargetSiteName: out.Job.TargetSite,
Errors: out.Job.Errors,
}, nil
}
func (rh *runnerHandler) GetJobs(ctx context.Context, cmd *api.GetJobsCommand) (*api.JobsResult, error) {
out, err := rh.domain.GetJobs(ctx, runner.GetJobsInput{})
if err != nil {
return nil, status.Errorf(codes.Internal, "method GetJobs failed: %q", err.Error())
}
result := &api.JobsResult{
Jobs: []*api.JobResult{},
}
for _, j := range out.Jobs {
result.Jobs = append(result.Jobs, &api.JobResult{
Id: int32(j.ID),
AuctionsFound: int32(j.AuctionsFound),
CreatedTs: timestamppb.New(j.Started),
CompletedTs: timestamppb.New(j.Completed),
TargetSiteName: j.TargetSite,
Errors: j.Errors,
})
}
return result, nil
}

@ -2,33 +2,113 @@ package main
import (
"context"
"time"
"flag"
"fmt"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/domain/runner"
"git.vdhsn.com/barretthousen/barretthousen/src/lib/kernel"
"git.vdhsn.com/barretthousen/barretthousen/src/runner/internal"
"git.vdhsn.com/barretthousen/barretthousen/src/runner/internal/data"
"git.vdhsn.com/barretthousen/barretthousen/src/runner/internal/data/postgres"
"github.com/jackc/pgx/v4"
"go.uber.org/dig"
_ "embed"
_ "git.vdhsn.com/barretthousen/barretthousen/src/runner/internal/domain/liveauctioneers"
)
type (
RunnerApp struct {
LogLevel kernel.LogLevel `yaml:"log_level" env:"BH_LOG_LEVEL" env-default:"0" yaml-default:"0"`
Port int `yaml:"port" env:"RUNNER_PORT"`
DB_Service kernel.PostgresConnection `yaml:"db_service" env:"RUNNER_DB_SERVICE"`
DB_Migrate kernel.PostgresConnection `yaml:"db_migrate" env:"RUNNER_DB_MIGRATE"`
}
)
var migrate = flag.Bool("migrate", false, "migrates postgres db")
//go:embed internal/data/postgres/schema.sql
var dbMigrateScript string
func main() {
kernel.Run(context.Background(), kernel.AppConfig{
App: &RunnerApp{},
flag.Parse()
kernel.Run(context.Background(), &RunnerApp{
LogLevel: kernel.LevelTrace,
Port: 5001,
})
}
type RunnerApp struct{}
func (app *RunnerApp) Start(ctx context.Context) error {
t := time.NewTicker(time.Second)
for {
select {
case <-ctx.Done():
return nil
case <-t.C:
kernel.TraceLog.Println("waiting")
t.Reset(time.Second)
if *migrate {
kernel.InfoLog.Printf("running db migrations on %v", app.DB_Migrate)
kernel.InfoLog.Printf("MIGRATION SCRIPT:\n%q", dbMigrateScript)
if err := kernel.Migrate(ctx, app.DB_Migrate, dbMigrateScript); err != nil {
return fmt.Errorf("could not execute db migration: %w", err)
}
return nil
}
ioc := dig.New()
var err error
if err = ioc.Provide(func() kernel.PostgresConnection {
return app.DB_Service
}); err != nil {
return err
}
if err = ioc.Provide(func(pgCfg kernel.PostgresConnection) (*pgx.Conn, error) {
return kernel.NewDBConnection(ctx, pgCfg)
}); err != nil {
return err
}
if err = ioc.Provide(func(pgConn *pgx.Conn) *postgres.Queries {
return postgres.New(pgConn)
}); err != nil {
return err
}
if err = ioc.Provide(func(queries *postgres.Queries) runner.Storage {
return &data.PGRunnerStorage{Queries: queries}
}); err != nil {
return err
}
if err = ioc.Provide(func() runner.CatalogService {
return &runner.CatalogServiceStub{}
}); err != nil {
return err
}
if err = ioc.Provide(func(css runner.CatalogService, rs runner.Storage) *runner.Domain {
return &runner.Domain{
Storage: rs,
CatalogService: css,
}
}); err != nil {
return err
}
return ioc.Invoke(func(d *runner.Domain) error {
runnerService := internal.NewRunnerServer(d)
if _, err := kernel.StartGRPCServer(ctx, app.Port, runnerService); err != nil {
return err
}
return nil
})
}
func (app *RunnerApp) OnStop(ctx context.Context) {
if err := kernel.StopGRPCServer(); err != nil {
kernel.ErrorLog.Printf("could not gracefully stop GRPC server: %v", err)
}
}
func (app *RunnerApp) GetLogLevel() kernel.LogLevel { return app.LogLevel }

Loading…
Cancel
Save