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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ backend/.env
frontend/node_modules
frontend/dist
frontend/.tanstack
*.exe
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,23 @@ jobs:
matrix:
include:
- name: hub
base_image: gcr.io/distroless/base-nossl-debian13:nonroot
- name: agent
base_image: gcr.io/distroless/base-nossl-debian13:latest
Comment thread
timokoessler marked this conversation as resolved.
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1

- name: Verify distroless base image signature
run: |
cosign verify \
${{ matrix.base_image }} \
--certificate-oidc-issuer https://accounts.google.com \
--certificate-identity keyless@distroless.iam.gserviceaccount.com

- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0

Expand Down Expand Up @@ -82,6 +94,7 @@ jobs:
type=raw,value=main,enable=${{ github.event_name == 'workflow_dispatch' }}

- name: Build and push ${{ matrix.name }} image
id: build
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
Expand All @@ -98,3 +111,8 @@ jobs:
cache-to: type=gha,mode=max
provenance: true
sbom: true

- name: Sign ${{ matrix.name }} image
run: |
cosign sign --yes \
${{ env.IMAGE_PREFIX }}/${{ matrix.name }}@${{ steps.build.outputs.digest }}
8 changes: 4 additions & 4 deletions agent.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
FROM bufbuild/buf:1.68 AS buf

FROM golang:1.26-alpine AS builder
FROM golang:1.26-trixie AS builder

ARG VERSION=dev
ARG COMMIT=none
ARG BUILD_DATE=unknown

RUN apk add --no-cache gcc musl-dev

WORKDIR /src
COPY backend/go.mod backend/go.sum ./
RUN go mod download
Expand All @@ -26,7 +24,9 @@ RUN CGO_ENABLED=1 go build \
-X github.com/OrcaCD/orca-cd/internal/version.BuildDate=${BUILD_DATE}" \
-o /bin/agent ./cmd/agent

FROM alpine:3.23
# Not using nonroot variant as agent has access to the docker socket which is basically root access
# Changing to nonroot user would complicate setup for users with minimal security benefits
FROM gcr.io/distroless/base-nossl-debian13:latest

WORKDIR /app

Expand Down
3 changes: 3 additions & 0 deletions backend/cmd/hub/backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ func setupBackupTestEnv(t *testing.T) {
if err := os.Chdir(workDir); err != nil {
t.Fatalf("failed to change working directory: %v", err)
}
if err := os.MkdirAll(filepath.Join(workDir, "data"), 0750); err != nil {
t.Fatalf("failed to create data dir: %v", err)
}
t.Cleanup(func() {
_ = db.Close()
_ = os.Chdir(originalWD)
Expand Down
43 changes: 43 additions & 0 deletions backend/internal/hub/data_dir.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package hub

import (
"fmt"
"os"
"runtime"
)
Comment thread
timokoessler marked this conversation as resolved.

func initDataDir() error {
if err := os.MkdirAll("data", 0750); err != nil {
return err
}
return checkDataDirWritable()
}

func checkDataDirWritable() error {
return checkWritable("data")
}

func checkWritable(dir string) error {
f, err := os.CreateTemp(dir, ".write-check-*")
if err == nil {
err = f.Close()
if err != nil {
return err
}
err = os.Remove(f.Name())
if err != nil {
return err
}
return nil
}
if !os.IsPermission(err) {
return err
}
if runtime.GOOS == "windows" {
return fmt.Errorf("directory is not writable, check the folder permissions")
}
return fmt.Errorf(
"directory is not writable, run: sudo chown -R %d:%d ./data",
os.Getuid(), os.Getgid(),
)
}
23 changes: 23 additions & 0 deletions backend/internal/hub/data_dir_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package hub

import (
"os"
"testing"
)

func TestCheckWritable_WritableDir(t *testing.T) {
dir := t.TempDir()
if err := checkWritable(dir); err != nil {
t.Fatalf("expected nil for writable directory, got %v", err)
}
}

func TestCheckWritable_NonExistentDir(t *testing.T) {
err := checkWritable(t.TempDir() + "/does-not-exist")
if err == nil {
t.Fatal("expected error for non-existent directory, got nil")
}
if os.IsPermission(err) {
t.Errorf("expected a non-permission error for missing directory, got: %v", err)
}
}
34 changes: 34 additions & 0 deletions backend/internal/hub/data_dir_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//go:build !windows

package hub

import (
"os"
"strings"
"testing"
)

func TestCheckWritable_ReadOnlyDir(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("skipping: chmod has no effect when running as root")
}
dir := t.TempDir()
if err := os.Chmod(dir, 0o400); err != nil {
t.Fatalf("chmod: %v", err)
}
// Restore before t.TempDir cleanup so the directory can be removed.
// t.Cleanup runs in LIFO order, so this runs before the TempDir cleanup.
t.Cleanup(func() {
if err := os.Chmod(dir, 0o700); err != nil { //nolint:gosec // restore perms so t.TempDir cleanup can remove it
t.Errorf("cleanup chmod: %v", err)
}
})

err := checkWritable(dir)
if err == nil {
t.Fatal("expected error for read-only directory, got nil")
}
if !strings.Contains(err.Error(), "not writable") {
t.Errorf("error %q does not contain expected message", err)
}
}
4 changes: 0 additions & 4 deletions backend/internal/hub/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package db
import (
"embed"
"net/url"
"os"
"sync"
"time"

Expand Down Expand Up @@ -70,9 +69,6 @@ func configureSQLitePool(db *gorm.DB) error {

func Connect(newLogger zerolog.Logger, logLevel zerolog.Level, demo bool) error {
logger = newLogger
if err := os.MkdirAll("data", 0750); err != nil {
return err
}

gormLogLevel := gormlogger.Error
if logLevel <= zerolog.DebugLevel {
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/hub/db/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,9 @@ func TestConnect_DemoModeSeedsDataOnce(t *testing.T) {
if err := os.Chdir(workDir); err != nil {
t.Fatalf("failed to change working directory: %v", err)
}
if err := os.MkdirAll(filepath.Join(workDir, "data"), 0750); err != nil {
t.Fatalf("failed to create data dir: %v", err)
}
t.Cleanup(func() {
_ = os.Chdir(originalWD)
if DB != nil {
Expand Down
5 changes: 5 additions & 0 deletions backend/internal/hub/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ func Run(cfg Config) error {
return err
}

if err := initDataDir(); err != nil {
Log.Error().Err(err).Msg("failed to initialize data directory")
return err
}

Comment thread
timokoessler marked this conversation as resolved.
dbLogger := Log.With().Str("component", "gorm").Logger()
err := db.Connect(dbLogger, cfg.LogLevel, cfg.Demo)
if err != nil {
Expand Down
18 changes: 7 additions & 11 deletions hub.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,12 @@ RUN npm run build

FROM bufbuild/buf:1.68 AS buf

FROM golang:1.26-alpine AS builder
FROM golang:1.26-trixie AS builder

ARG VERSION=dev
ARG COMMIT=none
ARG BUILD_DATE=unknown

RUN apk add --no-cache gcc musl-dev sqlite-dev

WORKDIR /src
COPY backend/go.mod backend/go.sum ./
RUN go mod download
Expand All @@ -33,19 +31,17 @@ RUN buf generate

RUN CGO_ENABLED=1 go build \
-ldflags "-s -w \
-X github.com/OrcaCD/orca-cd/internal/version.Version=${VERSION} \
-X github.com/OrcaCD/orca-cd/internal/version.Commit=${COMMIT} \
-X github.com/OrcaCD/orca-cd/internal/version.BuildDate=${BUILD_DATE}" \
-X github.com/OrcaCD/orca-cd/internal/version.Version=${VERSION} \
-X github.com/OrcaCD/orca-cd/internal/version.Commit=${COMMIT} \
-X github.com/OrcaCD/orca-cd/internal/version.BuildDate=${BUILD_DATE}" \
-o /bin/hub ./cmd/hub

FROM alpine:3.23
FROM gcr.io/distroless/base-nossl-debian13:nonroot

WORKDIR /app

RUN apk add --no-cache ca-certificates sqlite-libs

COPY --from=builder /bin/hub /app/hub
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
COPY --from=builder --chown=nonroot:nonroot /bin/hub /app/hub
COPY --from=frontend-builder --chown=nonroot:nonroot /app/frontend/dist ./frontend/dist

EXPOSE 8080

Expand Down
Loading