Skip to content
Open
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
20 changes: 17 additions & 3 deletions .github/workflows/web-test-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ name: Unit tests for web code
on:
pull_request:
paths:
- ".github/workflows/web-test.yml"
- ".github/workflows/web-test-unit-tests.yml"
- ".nvmrc"
- "package.json"
- "package-lock.json"
- "web-admin/**"
- "web-common/**"
- "web-local/**"
Expand All @@ -19,10 +22,17 @@ jobs:
with:
filters: |
admin:
- ".github/workflows/web-test.yml"
- ".github/workflows/web-test-unit-tests.yml"
- ".nvmrc"
- "package.json"
- "package-lock.json"
- "web-admin/**"
- "web-common/**"
common:
- ".github/workflows/web-test.yml"
- ".github/workflows/web-test-unit-tests.yml"
- ".nvmrc"
- "package.json"
- "package-lock.json"
- "web-common/**"

- name: Set up NodeJS
Expand All @@ -40,3 +50,7 @@ jobs:
- name: Run web-common unit tests
if: steps.filter.outputs.common == 'true'
run: npm run test -w web-common

- name: Run web-admin unit tests
if: steps.filter.outputs.admin == 'true'
run: npm run test:unit -w web-admin
5 changes: 3 additions & 2 deletions admin/deployments.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,10 @@ func (s *Service) TeardownDeployment(ctx context.Context, depl *database.Deploym
}

// UpdateDeploymentsForProject updates the deployments of a project.
// An empty environment updates deployments in all environments.
// Care must be taken to avoid one broken deployment from blocking updates to other healthy deployments.
func (s *Service) UpdateDeploymentsForProject(ctx context.Context, p *database.Project) error {
ds, err := s.DB.FindDeploymentsForProject(ctx, p.ID, "", "")
func (s *Service) UpdateDeploymentsForProject(ctx context.Context, p *database.Project, environment string) error {
ds, err := s.DB.FindDeploymentsForProject(ctx, p.ID, environment, "")
if err != nil {
return err
}
Expand Down
111 changes: 111 additions & 0 deletions admin/project_slots_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package admin

import (
"context"
"sync"
"testing"

"github.com/rilldata/rill/admin/database"
"github.com/rilldata/rill/admin/jobs"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)

func TestUpdateProjectSlots(t *testing.T) {
tests := []struct {
name string
prodSlots int
devSlots int
subpath string
expectedIDs []string
}{
{name: "unchanged", prodSlots: 2, devSlots: 2},
{name: "production only", prodSlots: 4, devSlots: 2, expectedIDs: []string{"prod-deployment", "prod-branch-deployment"}},
{name: "development only", prodSlots: 2, devSlots: 4, expectedIDs: []string{"dev-deployment", "dev-branch-deployment"}},
{name: "both environments", prodSlots: 4, devSlots: 4, expectedIDs: []string{"prod-deployment", "prod-branch-deployment", "dev-deployment", "dev-branch-deployment"}},
{name: "shared configuration", prodSlots: 2, devSlots: 2, subpath: "project", expectedIDs: []string{"prod-deployment", "prod-branch-deployment", "dev-deployment", "dev-branch-deployment"}},
{name: "shared configuration with production slots", prodSlots: 4, devSlots: 2, subpath: "project", expectedIDs: []string{"prod-deployment", "prod-branch-deployment", "dev-deployment", "dev-branch-deployment"}},
{name: "shared configuration with development slots", prodSlots: 2, devSlots: 4, subpath: "project", expectedIDs: []string{"prod-deployment", "prod-branch-deployment", "dev-deployment", "dev-branch-deployment"}},
}
for _, tt := range tests {
for _, status := range []database.DeploymentStatus{database.DeploymentStatusRunning, database.DeploymentStatusStopped} {
t.Run(tt.name+"/"+status.String(), func(t *testing.T) {
project := &database.Project{ID: "project", PrimaryBranch: "main", ProdSlots: 2, DevSlots: 2}
db := &slotUpdateDB{
project: project,
deployments: []*database.Deployment{
{ID: "prod-deployment", Environment: "prod", Branch: "main", DesiredStatus: status},
{ID: "prod-branch-deployment", Environment: "prod", Branch: "release", DesiredStatus: status},
{ID: "dev-deployment", Environment: "dev", Branch: "feature", DesiredStatus: status},
{ID: "dev-branch-deployment", Environment: "dev", Branch: "another-feature", DesiredStatus: status},
},
desiredStatuses: make(map[string]database.DeploymentStatus),
}
jobClient := &slotUpdateJobs{}
svc := &Service{DB: db, Jobs: jobClient, Logger: zap.NewNop()}

updated, err := svc.UpdateProject(t.Context(), project, &database.UpdateProjectOptions{
PrimaryBranch: "main", ProdSlots: tt.prodSlots, DevSlots: tt.devSlots, Subpath: tt.subpath,
})
require.NoError(t, err)
require.Equal(t, tt.devSlots, updated.DevSlots)
require.Equal(t, tt.prodSlots, updated.ProdSlots)
require.Equal(t, tt.subpath, updated.Subpath)
require.ElementsMatch(t, tt.expectedIDs, jobClient.deploymentIDs)
expectedStatuses := make(map[string]database.DeploymentStatus)
for _, id := range tt.expectedIDs {
expectedStatuses[id] = status
}
// Unaffected deployments must not be touched, and hibernated deployments must stay stopped.
require.Equal(t, expectedStatuses, db.desiredStatuses)
})
}
}
}

// Only the database and queue operations used by UpdateProject are implemented.
type slotUpdateDB struct {
database.DB
mu sync.Mutex
project *database.Project
deployments []*database.Deployment
desiredStatuses map[string]database.DeploymentStatus
}

func (db *slotUpdateDB) FindDeploymentsForProject(_ context.Context, _, environment, branch string) ([]*database.Deployment, error) {
var deployments []*database.Deployment
for _, d := range db.deployments {
if (environment == "" || d.Environment == environment) && (branch == "" || d.Branch == branch) {
deployments = append(deployments, d)
}
}
return deployments, nil
}

func (db *slotUpdateDB) UpdateProject(_ context.Context, _ string, opts *database.UpdateProjectOptions) (*database.Project, error) {
updated := *db.project
updated.ProdSlots = opts.ProdSlots
updated.DevSlots = opts.DevSlots
updated.Subpath = opts.Subpath
return &updated, nil
}

func (db *slotUpdateDB) UpdateDeploymentDesiredStatus(_ context.Context, id string, desired database.DeploymentStatus) (*database.Deployment, error) {
db.mu.Lock()
defer db.mu.Unlock()
db.desiredStatuses[id] = desired
return &database.Deployment{ID: id, DesiredStatus: desired}, nil
}

type slotUpdateJobs struct {
jobs.Client
mu sync.Mutex
deploymentIDs []string
}

func (j *slotUpdateJobs) ReconcileDeployment(_ context.Context, id string) (*jobs.InsertResult, error) {
j.mu.Lock()
defer j.mu.Unlock()
j.deploymentIDs = append(j.deploymentIDs, id)
return &jobs.InsertResult{}, nil
}
25 changes: 17 additions & 8 deletions admin/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,7 @@ func (s *Service) UpdateProject(ctx context.Context, oldProj *database.Project,
return nil, err
}

impactsDeployments := (oldProj.ProdVersion != opts.ProdVersion) ||
(oldProj.ProdSlots != opts.ProdSlots) ||
impactsAllDeployments := (oldProj.ProdVersion != opts.ProdVersion) ||
(oldProj.Name != opts.Name) ||
(oldProj.Subpath != opts.Subpath) ||
(oldProj.PrimaryBranch != opts.PrimaryBranch) ||
Expand All @@ -206,7 +205,9 @@ func (s *Service) UpdateProject(ctx context.Context, oldProj *database.Project,
!reflect.DeepEqual(oldProj.ArchiveAssetID, opts.ArchiveAssetID) ||
!reflect.DeepEqual(oldProj.OverrideDiskGB, opts.OverrideDiskGB)

if !impactsDeployments {
prodSlotsChanged := oldProj.ProdSlots != opts.ProdSlots
devSlotsChanged := oldProj.DevSlots != opts.DevSlots
if !impactsAllDeployments && !prodSlotsChanged && !devSlotsChanged {
return proj, nil
}

Expand Down Expand Up @@ -246,9 +247,17 @@ func (s *Service) UpdateProject(ctx context.Context, oldProj *database.Project,
}
}

// TODO: changing prod related fields like slots, branch etc should only impact prod deployments, but for now we update all deployments
// NOTE: there is no way to change dev-slots right now
err = s.UpdateDeploymentsForProject(ctx, proj)
// Slot-only changes should only reconcile deployments in the affected environment.
// An empty environment updates all deployments for shared changes or changes to both slot counts.
environment := ""
if !impactsAllDeployments {
if prodSlotsChanged && !devSlotsChanged {
environment = "prod"
} else if devSlotsChanged && !prodSlotsChanged {
environment = "dev"
}
}
err = s.UpdateDeploymentsForProject(ctx, proj, environment)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -294,7 +303,7 @@ func (s *Service) UpdateProjectVariables(ctx context.Context, project *database.
// Update deployments
s.Logger.Info("update project variables: updating deployments", observability.ZapCtx(ctx))

err = s.UpdateDeploymentsForProject(ctx, project)
err = s.UpdateDeploymentsForProject(ctx, project, "")
if err != nil {
return err
}
Expand All @@ -315,7 +324,7 @@ func (s *Service) UpdateOrgDeploymentAnnotations(ctx context.Context, org *datab
}

for _, proj := range projs {
err := s.UpdateDeploymentsForProject(ctx, proj)
err := s.UpdateDeploymentsForProject(ctx, proj, "")
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions admin/server/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ const prodDeplTTL = 14 * 24 * time.Hour

// defaultProdSlots and defaultDevSlots are the slot counts applied when a CreateProject
// request omits them (e.g. the UI, or older CLIs that don't pass these fields).
const defaultProdSlots = 4
const defaultProdSlots = 2

const defaultDevSlots = 4
const defaultDevSlots = 2

// runtimeAccessTokenTTL is the validity duration of JWTs issued for runtime access when calling GetProject.
// This TTL is not used for tokens created for internal communication between the admin and runtime services.
Expand Down
28 changes: 28 additions & 0 deletions admin/server/projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@ import (
"github.com/stretchr/testify/require"
)

func TestCreateProjectSlots(t *testing.T) {
fix := testadmin.New(t)
_, client := fix.NewUser(t)
org, err := client.CreateOrganization(t.Context(), &adminv1.CreateOrganizationRequest{Name: randomName()})
require.NoError(t, err)

for _, tt := range []struct {
name string
prod, dev int64
wantProd, wantDev int64
}{
{name: "defaults", wantProd: 2, wantDev: 2},
{name: "explicit", prod: 1, dev: 3, wantProd: 1, wantDev: 3},
{name: "default-prod", dev: 1, wantProd: 2, wantDev: 1},
{name: "default-dev", prod: 3, wantProd: 3, wantDev: 2},
} {
t.Run(tt.name, func(t *testing.T) {
resp, err := client.CreateProject(t.Context(), &adminv1.CreateProjectRequest{
Org: org.Organization.Name, Project: tt.name,
ProdSlots: tt.prod, DevSlots: tt.dev, SkipDeploy: true,
})
require.NoError(t, err)
require.Equal(t, tt.wantProd, resp.Project.ProdSlots)
require.Equal(t, tt.wantDev, resp.Project.DevSlots)
})
}
}

func TestProjectVariables(t *testing.T) {
fix := testadmin.New(t)

Expand Down
8 changes: 4 additions & 4 deletions cli/pkg/local/slots.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@ import "github.com/rilldata/rill/cli/pkg/cmdutil"
// DefaultProdSlots returns the prod slot count to request from the admin API.
//
// Returns 0 for released builds, which signals the admin server to apply its own default.
// Dev builds request a single slot to keep local deployments small.
// Dev builds request two slots to match the default for new deployments.
//
// A slot represents the following resources:
// - 1 CPU core
// - 4 GB of memory
// - 40 GB of storage
func DefaultProdSlots(ch *cmdutil.Helper) int {
if ch.IsDev() {
return 1
return 2
}
return 0
}

// DefaultDevSlots returns the dev slot count to request from the admin API.
//
// Returns 0 for released builds, which signals the admin server to apply its own default.
// Dev builds request a single slot to keep local deployments small.
// Dev builds request two slots to match the default for new deployments.
//
// A slot represents the following resources:
// - 1 CPU core
// - 4 GB of memory
// - 40 GB of storage
func DefaultDevSlots(ch *cmdutil.Helper) int {
if ch.IsDev() {
return 1
return 2
}
return 0
}
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion web-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,19 @@
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.55.0",
"@tanstack/svelte-query": "~5.76.0",
"tanstack-table-8-svelte-5": "^0.1.2",
"@testing-library/svelte": "^5.2.7",
"autoprefixer": "^10.4.20",
"axios": "1.13.6",
"cronstrue": "^2.41.0",
"dotenv": "^16.5.0",
"jsdom": "^26.0.0",
"orval": "^7.8.0",
"postcss": "^8.4.47",
"svelte": "^5.0.0",
"svelte-check": "^4.0.4",
"svelte-preprocess": "^6.0.3",
"tailwindcss": "^3.4.13",
"tanstack-table-8-svelte-5": "^0.1.2",
"vitest": "^4.1.1",
"web-common": "*"
},
Expand Down
Loading
Loading