Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"golang.go",
"unifiedjs.vscode-mdx",
"a-h.templ",
"redhat.vscode-yaml"
"redhat.vscode-yaml",
"hashicorp.hcl",
"fredwangwang.vscode-hcl-format"

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error

fredwangwang is not a recognized word. (unrecognized-spelling)
]
}
}
Expand Down
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.env
*.deb
*.rpm

# Additional package locks
pnpm-lock.yaml
yarn.lock

# Go binaries and test artifacts
main
*.test

node_modules

# MacOS
.DS_store

# Intellij
.idea

# how does this get here
doc/VERSION

web/static/js/*
!web/static/js/.gitignore
29 changes: 27 additions & 2 deletions .github/workflows/docker-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Docker image builds (pull requests)

on:
pull_request:
branches: [ "main" ]
branches: ["main"]

env:
DOCKER_METADATA_SET_OUTPUT_ENV: "true"
Expand All @@ -11,7 +11,32 @@ permissions:
contents: read

jobs:
build:
buildx-bake:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-tags: true
fetch-depth: 0
persist-credentials: false

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0

- name: Build and push
id: build
uses: docker/bake-action@76f9fa3a758507623da19f6092dc4089a7e61592 # v6.6.0
with:
source: .
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
set: |
osiris.tags=ttl.sh/techaro/pr-${{ github.event.number }}/osiris
osiris.tags+=ttl.sh/techaro/pr-${{ github.event.number }}/osiris:24h

containerbuild:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
Expand Down
4 changes: 3 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"golang.go",
"unifiedjs.vscode-mdx",
"a-h.templ",
"redhat.vscode-yaml"
"redhat.vscode-yaml",
"hashicorp.hcl",

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error

hashicorp is not a recognized word. (unrecognized-spelling)
"fredwangwang.vscode-hcl-format"

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error

fredwangwang is not a recognized word. (unrecognized-spelling)
]
}
48 changes: 48 additions & 0 deletions cmd/osiris/internal/config/bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package config

import (
"errors"
"fmt"
"net"
)

var (
ErrCantBindToPort = errors.New("bind: can't bind to host:port")
)

type Bind struct {
HTTP string `hcl:"http"`
HTTPS string `hcl:"https"`
Metrics string `hcl:"metrics"`
}

func (b *Bind) Valid() error {
var errs []error

ln, err := net.Listen("tcp", b.HTTP)
if err != nil {
errs = append(errs, fmt.Errorf("%w %q: %w", ErrCantBindToPort, b.HTTP, err))
} else {
defer ln.Close()
}

ln, err = net.Listen("tcp", b.HTTPS)
if err != nil {
errs = append(errs, fmt.Errorf("%w %q: %w", ErrCantBindToPort, b.HTTPS, err))
} else {
defer ln.Close()
}

ln, err = net.Listen("tcp", b.Metrics)
if err != nil {
errs = append(errs, fmt.Errorf("%w %q: %w", ErrCantBindToPort, b.Metrics, err))
} else {
defer ln.Close()
}

if len(errs) != 0 {
return errors.Join(errs...)
}

return nil
}
55 changes: 55 additions & 0 deletions cmd/osiris/internal/config/bind_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package config

import (
"errors"
"net"
"testing"
)

func TestBindValid(t *testing.T) {
for _, tt := range []struct {
name string
precondition func(t *testing.T)
bind Bind
err error
}{
{
name: "basic",
precondition: nil,
bind: Bind{
HTTP: ":8081",
HTTPS: ":8082",
Metrics: ":8083",
},
err: nil,
},
{
name: "reused ports",
precondition: func(t *testing.T) {
ln, err := net.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })
},
bind: Bind{
HTTP: ":8081",
HTTPS: ":8081",
Metrics: ":8081",
},
err: ErrCantBindToPort,
},
} {
t.Run(tt.name, func(t *testing.T) {
if tt.precondition != nil {
tt.precondition(t)
}

if err := tt.bind.Valid(); !errors.Is(err, tt.err) {
t.Logf("want: %v", tt.err)
t.Logf("got: %v", err)
t.Error("got wrong error from validation function")
}
})
}
}
31 changes: 31 additions & 0 deletions cmd/osiris/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package config

import (
"errors"
"fmt"
)

type Toplevel struct {
Bind Bind `hcl:"bind,block"`
Domains []Domain `hcl:"domain,block"`
}

func (t *Toplevel) Valid() error {
var errs []error

if err := t.Bind.Valid(); err != nil {
errs = append(errs, fmt.Errorf("invalid bind block:\n%w", err))
}

for _, d := range t.Domains {
if err := d.Valid(); err != nil {
errs = append(errs, fmt.Errorf("when parsing domain %s: %w", d.Name, err))
}
}

if len(errs) != 0 {
return fmt.Errorf("invalid configuration file:\n%w", errors.Join(errs...))
}

return nil
}
66 changes: 66 additions & 0 deletions cmd/osiris/internal/config/domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package config

import (
"errors"
"fmt"
"net/url"

"golang.org/x/net/idna"
)

var (
ErrInvalidDomainName = errors.New("domain: name is invalid")
ErrInvalidDomainTLSConfig = errors.New("domain: TLS config is invalid")
ErrInvalidURL = errors.New("invalid URL")
ErrInvalidURLScheme = errors.New("URL has invalid scheme")
)

type Domain struct {
Name string `hcl:"name,label"`
TLS TLS `hcl:"tls,block"`
Target string `hcl:"target"`
InsecureSkipVerify bool `hcl:"insecure_skip_verify,optional"`
HealthTarget string `hcl:"health_target"`
}

func (d Domain) Valid() error {
var errs []error

if _, err := idna.Lookup.ToASCII(d.Name); err != nil {
errs = append(errs, fmt.Errorf("%w %q: %w", ErrInvalidDomainName, d.Name, err))
}

if err := d.TLS.Valid(); err != nil {
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidDomainTLSConfig, err))
}

if err := isURLValid(d.Target); err != nil {
errs = append(errs, fmt.Errorf("target has %w %q: %w", ErrInvalidURL, d.Target, err))
}

if err := isURLValid(d.HealthTarget); err != nil {
errs = append(errs, fmt.Errorf("health_target has %w %q: %w", ErrInvalidURL, d.HealthTarget, err))
}

if len(errs) != 0 {
return errors.Join(errs...)
}

return nil
}

func isURLValid(input string) error {
u, err := url.Parse(input)
if err != nil {
return err
}

switch u.Scheme {
case "http", "https", "h2c", "unix":
// do nothing
default:
return fmt.Errorf("%w %s has scheme %s (want http, https, h2c, unix)", ErrInvalidURLScheme, input, u.Scheme)
}

return nil
}
89 changes: 89 additions & 0 deletions cmd/osiris/internal/config/domain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package config

import (
"errors"
"testing"
)

func TestDomainValid(t *testing.T) {
for _, tt := range []struct {
name string
input Domain
err error
}{
{
name: "simple happy path",
input: Domain{
Name: "anubis.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/selfsigned.crt",

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error test

selfsigned is not a recognized word. (unrecognized-spelling)
Key: "./testdata/tls/selfsigned.key",

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error test

selfsigned is not a recognized word. (unrecognized-spelling)
},
Target: "http://localhost:3000",
HealthTarget: "http://localhost:9091/healthz",
},
},
{
name: "invalid domain name",
input: Domain{
Name: "\uFFFD.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/selfsigned.crt",

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error test

selfsigned is not a recognized word. (unrecognized-spelling)
Key: "./testdata/tls/selfsigned.key",

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error test

selfsigned is not a recognized word. (unrecognized-spelling)
},
Target: "http://localhost:3000",
HealthTarget: "http://localhost:9091/healthz",
},
err: ErrInvalidDomainName,
},
{
name: "invalid tls config",
input: Domain{
Name: "anubis.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/invalid.crt",
Key: "./testdata/tls/invalid.key",
},
Target: "http://localhost:3000",
HealthTarget: "http://localhost:9091/healthz",
},
err: ErrInvalidDomainTLSConfig,
},
{
name: "invalid URL",
input: Domain{
Name: "anubis.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/selfsigned.crt",

Check failure

Code scanning / check-spelling

Unrecognized Spelling Error test

selfsigned is not a recognized word. (unrecognized-spelling)
Key: "./testdata/tls/selfsigned.key",
},
Target: "file://[::1:3000",
HealthTarget: "file://[::1:9091/healthz",
},
err: ErrInvalidURL,
},
{
name: "wrong URL scheme",
input: Domain{
Name: "anubis.techaro.lol",
TLS: TLS{
Cert: "./testdata/tls/selfsigned.crt",
Key: "./testdata/tls/selfsigned.key",
},
Target: "file://localhost:3000",
HealthTarget: "file://localhost:9091/healthz",
},
err: ErrInvalidURLScheme,
},
} {
t.Run(tt.name, func(t *testing.T) {
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
t.Logf("want: %v", tt.err)
t.Logf("got: %v", err)
t.Error("got wrong error from validation function")
} else {
t.Log(err)
}
})
}
}
1 change: 1 addition & 0 deletions cmd/osiris/internal/config/testdata/tls/invalid.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar
1 change: 1 addition & 0 deletions cmd/osiris/internal/config/testdata/tls/invalid.key
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aorsentaeiorsntoiearnstoieanrsoietnaioresntoeiar
Loading
Loading