Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 8 additions & 8 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,31 @@ jobs:
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.workflow_run.head_sha }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable=${{ github.event.workflow_run.head_branch == 'main' }},suffix=-${{ matrix.arch }}
type=raw,value=${{ github.event.workflow_run.head_branch }},enable=${{ startsWith(github.event.workflow_run.head_branch, 'v') }},suffix=-${{ matrix.arch }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4

- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: .
file: docker/Dockerfile
Expand All @@ -69,18 +69,18 @@ jobs:
steps:
- name: Docker metadata
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable=${{ github.event.workflow_run.head_branch == 'main' }}
type=raw,value=${{ github.event.workflow_run.head_branch }},enable=${{ startsWith(github.event.workflow_run.head_branch, 'v') }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4

- name: Login to GHCR
uses: docker/login-action@v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ jobs:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.26"

- name: Install SQLite dev headers
run: sudo apt-get install -y libsqlite3-dev

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
with:
version: latest
args: --timeout=5m
2 changes: 1 addition & 1 deletion .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@v6
- uses: release-drafter/release-drafter@67e173cadb2fbd3de94f4a861e0c48c913b462ae # v6
with:
config-name: release-drafter.yml
env:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/smithy-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: "1.26"

Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:

- name: Create Pull Request
if: steps.changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v8
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
with:
commit-message: "chore: sync Smithy models and regenerate code"
title: "chore: weekly Smithy model sync"
Expand Down
6 changes: 5 additions & 1 deletion docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,20 @@ RUN go build -o /codegen ./cmd/codegen

# Stage 3: Runtime
FROM alpine:3.20
RUN apk add --no-cache sqlite-libs ca-certificates
RUN apk add --no-cache sqlite-libs ca-certificates su-exec
RUN adduser -D -H -h /app appuser
WORKDIR /app
COPY --from=go-builder /devcloud /app/devcloud
COPY --from=go-builder /codegen /app/codegen
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
COPY --from=web-builder /app/web/out /app/web/out
COPY devcloud.yaml /app/devcloud.yaml
COPY smithy-models/ /app/smithy-models/
COPY internal/codegen/templates/ /app/templates/
COPY docker/entrypoint.sh /app/entrypoint.sh
RUN chown -R appuser:appuser /app

EXPOSE 4747
VOLUME /app/data

ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["/app/devcloud", "-config", "/app/devcloud.yaml"]
3 changes: 3 additions & 0 deletions docker/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
FROM golang:1.26-alpine
RUN apk add --no-cache gcc musl-dev sqlite-dev
RUN adduser -D -H -h /app appuser
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN chown -R appuser:appuser /app
USER appuser
ENV CGO_ENABLED=1
RUN go build -o /app/devcloud ./cmd/devcloud
EXPOSE 4747
Expand Down
11 changes: 11 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh
set -e

# Ensure the data directory exists and is writable by appuser.
mkdir -p /app/data
owner=$(stat -c %U /app/data 2>/dev/null || echo "")
if [ "$owner" != "appuser" ]; then
chown appuser:appuser /app/data
fi

exec su-exec appuser "$@"
32 changes: 30 additions & 2 deletions internal/gateway/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package gateway
import (
"encoding/json"
"encoding/xml"
"mime"
"net/http"
"strings"

Expand Down Expand Up @@ -45,9 +46,36 @@ func (sr *ServiceRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for k, v := range resp.Headers {
w.Header().Set(k, v)
}
if resp.ContentType != "" {
w.Header().Set("Content-Type", resp.ContentType)
ct := resp.ContentType
if ct == "" {
ct = w.Header().Get("Content-Type")
}
if ct == "" {
ct = "application/octet-stream"
}
// Prevent XSS: this gateway serves AWS API responses only (JSON/XML),
// never user-facing HTML. Sanitize any attempt to serve HTML-like content.
ct = strings.TrimSpace(ct)
htmlLike := false
for _, p := range strings.Split(ct, ",") {
p = strings.TrimSpace(p)
if p == "" {
continue
}
mediaType, _, parseErr := mime.ParseMediaType(p)
if parseErr != nil {
continue
}
mtLower := strings.ToLower(mediaType)
if mtLower == "text/html" || mtLower == "application/xhtml+xml" || strings.HasSuffix(mtLower, "+html") {
htmlLike = true
break
}
}
if htmlLike {
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
ct = "text/plain; charset=utf-8"
}
w.Header().Set("Content-Type", ct)
w.WriteHeader(resp.StatusCode)
_, _ = w.Write(resp.Body)
}
Expand Down
43 changes: 41 additions & 2 deletions internal/shared/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
package shared

import (
"fmt"
"strings"

"github.com/skyoo2003/devcloud/internal/storage/sqlite"
)

Expand All @@ -18,8 +21,44 @@ type ResourceStore[T any] struct {
scanner func(Scanner) (T, error)
}

func NewResourceStore[T any](db *sqlite.Store, table, idCol, cols string, scanner func(Scanner) (T, error)) *ResourceStore[T] {
return &ResourceStore[T]{db: db, table: table, idCol: idCol, cols: cols, scanner: scanner}
func NewResourceStore[T any](db *sqlite.Store, table, idCol, cols string, scanner func(Scanner) (T, error)) (*ResourceStore[T], error) {
table = strings.TrimSpace(table)
idCol = strings.TrimSpace(idCol)
if err := validateIdentifier(table, "table"); err != nil {
return nil, err
}
if err := validateIdentifier(idCol, "idCol"); err != nil {
return nil, err
}
var validCols []string
for _, c := range strings.Split(cols, ",") {
c = strings.TrimSpace(c)
if c == "" {
continue
}
if err := validateIdentifier(c, "col"); err != nil {
return nil, err
}
validCols = append(validCols, c)
}
normalizedCols := strings.Join(validCols, ", ")
return &ResourceStore[T]{db: db, table: table, idCol: idCol, cols: normalizedCols, scanner: scanner}, nil
}

func validateIdentifier(s, kind string) error {
if len(s) == 0 {
return fmt.Errorf("shared: empty %s identifier", kind)
}
for _, r := range s {
isLower := r >= 'a' && r <= 'z'
isUpper := r >= 'A' && r <= 'Z'
isDigit := r >= '0' && r <= '9'
isUnderscore := r == '_'
if !isLower && !isUpper && !isDigit && !isUnderscore {
return fmt.Errorf("shared: invalid %s identifier: %q", kind, s)
}
}
return nil
}

func (s *ResourceStore[T]) DB() *sqlite.Store { return s.db }
Expand Down
94 changes: 93 additions & 1 deletion internal/shared/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ func newTestResourceStore(t *testing.T) *ResourceStore[testItem] {
db, err := sqlite.Open(dbPath, testMigrations)
require.NoError(t, err)
t.Cleanup(func() { _ = db.Close() })
return NewResourceStore[testItem](db, "items", "id", "id, name", testScanner)
rs, err := NewResourceStore[testItem](db, "items", "id", "id, name", testScanner)
require.NoError(t, err)
return rs
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
}

func TestResourceStore_GetNotFound(t *testing.T) {
Expand Down Expand Up @@ -110,3 +112,93 @@ func TestResourceStore_Count(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 1, n)
}

func newTestDB(t *testing.T) *sqlite.Store {
t.Helper()
dbPath := t.TempDir() + "/test.db"
db, err := sqlite.Open(dbPath, testMigrations)
require.NoError(t, err)
t.Cleanup(func() { _ = db.Close() })
return db
}

func TestNewResourceStore_InvalidTable(t *testing.T) {
db := newTestDB(t)
_, err := NewResourceStore[testItem](db, "DROP TABLE items; --", "id", "id", testScanner)
assert.ErrorContains(t, err, "invalid table identifier")
}

func TestNewResourceStore_InvalidIdCol(t *testing.T) {
db := newTestDB(t)
_, err := NewResourceStore[testItem](db, "items", "id; --", "id", testScanner)
assert.ErrorContains(t, err, "invalid idCol identifier")
}

func TestNewResourceStore_InvalidCol(t *testing.T) {
db := newTestDB(t)
_, err := NewResourceStore[testItem](db, "items", "id", "id, name; --", testScanner)
assert.ErrorContains(t, err, "invalid col identifier")
}

func TestNewResourceStore_EmptyTable(t *testing.T) {
db := newTestDB(t)
_, err := NewResourceStore[testItem](db, "", "id", "id", testScanner)
assert.ErrorContains(t, err, "empty table identifier")
}

func TestNewResourceStore_TrailingComma(t *testing.T) {
db := newTestDB(t)
rs, err := NewResourceStore[testItem](db, "items", "id", "id, name,", testScanner)
require.NoError(t, err)
require.NotNil(t, rs)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
}

func TestNewResourceStore_ValidIdentifiers(t *testing.T) {
db := newTestDB(t)

tests := []struct {
name string
tableName string
primary string
cols string
}{
{
name: "underscores",
tableName: "items",
primary: "id",
cols: "id, item_name, created_at",
},
{
name: "digits_in_identifiers",
tableName: "items",
primary: "id",
cols: "id, name2, col3_v1",
},
{
name: "trailing_comma",
tableName: "items",
primary: "id",
cols: "id, name,",
},
{
name: "whitespace_in_identifiers",
tableName: " items ",
primary: " id ",
cols: " id, name ",
},
{
name: "leading_comma_in_cols",
tableName: "items",
primary: "id",
cols: ", id, name",
},
}
Comment on lines +159 to +195
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Extend valid identifier table tests to cover whitespace and leading-comma cases

To better exercise the new normalization/parsing behavior, please add cases with surrounding whitespace in table/column identifiers (e.g. " items ", " id ", " id, name ") and a case with a leading comma in cols (e.g. ", id, name") to verify empty leading segments are ignored and valid segments are accepted.

Suggested change
tests := []struct {
name string
tableName string
primary string
cols string
}{
{
name: "underscores",
tableName: "items",
primary: "id",
cols: "id, item_name, created_at",
},
{
name: "digits_in_identifiers",
tableName: "items",
primary: "id",
cols: "id, name2, col3_v1",
},
{
name: "trailing_comma",
tableName: "items",
primary: "id",
cols: "id, name,",
},
}
tests := []struct {
name string
tableName string
primary string
cols string
}{
{
name: "underscores",
tableName: "items",
primary: "id",
cols: "id, item_name, created_at",
},
{
name: "digits_in_identifiers",
tableName: "items",
primary: "id",
cols: "id, name2, col3_v1",
},
{
name: "trailing_comma",
tableName: "items",
primary: "id",
cols: "id, name,",
},
{
name: "whitespace_in_identifiers",
tableName: " items ",
primary: " id ",
cols: " id, name ",
},
{
name: "leading_comma_in_cols",
tableName: "items",
primary: "id",
cols: ", id, name",
},
}


for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
rs, err := NewResourceStore[testItem](db, tc.tableName, tc.primary, tc.cols, testScanner)
require.NoError(t, err)
require.NotNil(t, rs)
})
}
}
Loading
Loading