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
36 changes: 27 additions & 9 deletions admin/server/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ import (

const devDeplTTL = 6 * time.Hour

const devSlots = 8

const prodDeplTTL = 14 * 24 * time.Hour

// runtimeAccessTokenTTL is the validity duration of JWTs issued for runtime access when calling GetProject.
Expand Down Expand Up @@ -574,6 +572,7 @@ func (s *Server) CreateProject(ctx context.Context, req *adminv1.CreateProjectRe
attribute.String("args.provisioner", req.Provisioner),
attribute.String("args.prod_version", req.ProdVersion),
attribute.Int64("args.prod_slots", req.ProdSlots),
attribute.Int64("args.dev_slots", req.DevSlots),
attribute.String("args.sub_path", req.Subpath),
attribute.String("args.primary_branch", req.PrimaryBranch),
attribute.String("args.git_remote", req.GitRemote),
Expand Down Expand Up @@ -616,6 +615,9 @@ func (s *Server) CreateProject(ctx context.Context, req *adminv1.CreateProjectRe
if org.QuotaSlotsPerDeployment >= 0 && int(req.ProdSlots) > org.QuotaSlotsPerDeployment {
return nil, status.Errorf(codes.FailedPrecondition, "quota exceeded: org can't provision more than %d slots per deployment; contact support for larger deployments", org.QuotaSlotsPerDeployment)
}
if org.QuotaSlotsPerDeployment >= 0 && int(req.DevSlots) > org.QuotaSlotsPerDeployment {
return nil, status.Errorf(codes.FailedPrecondition, "quota exceeded: org can't provision more than %d slots per deployment; contact support for larger deployments", org.QuotaSlotsPerDeployment)
}
if org.QuotaSlotsTotal >= 0 && usage.Slots+int(req.ProdSlots) > org.QuotaSlotsTotal {
return nil, status.Errorf(codes.FailedPrecondition, "quota exceeded: org %q is limited to %d total slots", org.Name, org.QuotaSlotsTotal)
}
Expand Down Expand Up @@ -664,7 +666,7 @@ func (s *Server) CreateProject(ctx context.Context, req *adminv1.CreateProjectRe
ProdVersion: req.ProdVersion,
ProdSlots: int(req.ProdSlots),
ProdTTLSeconds: prodTTL,
DevSlots: devSlots,
DevSlots: int(req.DevSlots),
DevTTLSeconds: devTTL,
}

Expand Down Expand Up @@ -788,6 +790,9 @@ func (s *Server) UpdateProject(ctx context.Context, req *adminv1.UpdateProjectRe
if req.ProdSlots != nil {
observability.AddRequestAttributes(ctx, attribute.Int64("args.prod_slots", *req.ProdSlots))
}
if req.DevSlots != nil {
observability.AddRequestAttributes(ctx, attribute.Int64("args.dev_slots", *req.DevSlots))
}
if req.ProdTtlSeconds != nil {
observability.AddRequestAttributes(ctx, attribute.Int64("args.prod_ttl_seconds", *req.ProdTtlSeconds))
}
Expand All @@ -812,22 +817,35 @@ func (s *Server) UpdateProject(ctx context.Context, req *adminv1.UpdateProjectRe
return nil, status.Error(codes.PermissionDenied, "does not have permission to manage project")
}

// Enforce slot quotas when ProdSlots is being changed (superusers bypass)
if req.ProdSlots != nil && !forceAccess {
// Enforce slot quotas when ProdSlots or DevSlots is being changed (superusers bypass)
if (req.ProdSlots != nil || req.DevSlots != nil) && !forceAccess {
org, err := s.admin.DB.FindOrganization(ctx, proj.OrganizationID)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if org.QuotaSlotsPerDeployment >= 0 && int(*req.ProdSlots) > org.QuotaSlotsPerDeployment {
return nil, status.Errorf(codes.FailedPrecondition, "quota exceeded: org can't provision more than %d slots per deployment; contact support for larger deployments", org.QuotaSlotsPerDeployment)
if req.ProdSlots != nil {
if org.QuotaSlotsPerDeployment >= 0 && int(*req.ProdSlots) > org.QuotaSlotsPerDeployment {
return nil, status.Errorf(codes.FailedPrecondition, "quota exceeded: org can't provision more than %d slots per deployment; contact support for larger deployments", org.QuotaSlotsPerDeployment)
}
}
if req.DevSlots != nil {
if org.QuotaSlotsPerDeployment >= 0 && int(*req.DevSlots) > org.QuotaSlotsPerDeployment {
return nil, status.Errorf(codes.FailedPrecondition, "quota exceeded: org can't provision more than %d slots per deployment; contact support for larger deployments", org.QuotaSlotsPerDeployment)
}
}
if org.QuotaSlotsTotal >= 0 {
usage, err := s.admin.DB.CountProjectsQuotaUsage(ctx, org.ID)
if err != nil {
return nil, err
}
// Calculate the delta: new slots minus current slots
delta := int(*req.ProdSlots) - proj.ProdSlots
delta := 0
if req.ProdSlots != nil {
delta += int(*req.ProdSlots) - proj.ProdSlots
}
if req.DevSlots != nil {
delta += int(*req.DevSlots) - proj.DevSlots
}
if delta > 0 && usage.Slots+delta > org.QuotaSlotsTotal {
return nil, status.Errorf(codes.FailedPrecondition, "quota exceeded: org %q is limited to %d total slots; contact support for larger deployments", org.Name, org.QuotaSlotsTotal)
}
Expand Down Expand Up @@ -928,7 +946,7 @@ func (s *Server) UpdateProject(ctx context.Context, req *adminv1.UpdateProjectRe
PrimaryDeploymentID: proj.PrimaryDeploymentID,
ProdSlots: int(valOrDefault(req.ProdSlots, int64(proj.ProdSlots))),
ProdTTLSeconds: prodTTLSeconds,
DevSlots: proj.DevSlots,
DevSlots: int(valOrDefault(req.DevSlots, int64(proj.DevSlots))),
DevTTLSeconds: proj.DevTTLSeconds,
Provisioner: valOrDefault(req.Provisioner, proj.Provisioner),
Annotations: proj.Annotations,
Expand Down
4 changes: 4 additions & 0 deletions cli/cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ func DeployCmd(ch *cmdutil.Helper) *cobra.Command {
deployCmd.Flags().StringVar(&opts.Provisioner, "provisioner", "", "Project provisioner")
deployCmd.Flags().StringVar(&opts.PrimaryBranch, "primary-branch", "", "Git branch to deploy from (default: the default Git branch)")
deployCmd.Flags().IntVar(&opts.Slots, "prod-slots", local.DefaultProdSlots(ch), "Slots to allocate for production deployments")
deployCmd.Flags().IntVar(&opts.DevSlots, "dev-slots", local.DefaultDevSlots(ch), "Slots to allocate for dev deployments")
deployCmd.Flags().BoolVar(&opts.PushEnv, "push-env", true, "Push local .env file to Rill Cloud")
if !ch.IsDev() {
if err := deployCmd.Flags().MarkHidden("prod-slots"); err != nil {
panic(err)
}
if err := deployCmd.Flags().MarkHidden("dev-slots"); err != nil {
panic(err)
}
}

deployCmd.Flags().BoolVar(&opts.Managed, "managed", false, "Create project using rill managed repo")
Expand Down
5 changes: 5 additions & 0 deletions cli/cmd/project/connect_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,15 @@ func GitPushCmd(ch *cmdutil.Helper) *cobra.Command {
deployCmd.Flags().StringVar(&opts.Provisioner, "provisioner", "", "Project provisioner")
deployCmd.Flags().StringVar(&opts.PrimaryBranch, "primary-branch", "", "Git branch to deploy from (default: the default Git branch)")
deployCmd.Flags().IntVar(&opts.Slots, "prod-slots", local.DefaultProdSlots(ch), "Slots to allocate for production deployments")
deployCmd.Flags().IntVar(&opts.DevSlots, "dev-slots", local.DefaultDevSlots(ch), "Slots to allocate for dev deployments")
deployCmd.Flags().BoolVar(&opts.PushEnv, "push-env", true, "Push local .env file to Rill Cloud")
if !ch.IsDev() {
if err := deployCmd.Flags().MarkHidden("prod-slots"); err != nil {
panic(err)
}
if err := deployCmd.Flags().MarkHidden("dev-slots"); err != nil {
panic(err)
}
}

return deployCmd
Expand Down Expand Up @@ -166,6 +170,7 @@ func ConnectGithubFlow(ctx context.Context, ch *cmdutil.Helper, opts *DeployOpts
Provisioner: opts.Provisioner,
ProdVersion: opts.ProdVersion,
ProdSlots: int64(opts.Slots),
DevSlots: int64(opts.DevSlots),
Subpath: opts.SubPath,
PrimaryBranch: opts.PrimaryBranch,
Public: opts.Public,
Expand Down
6 changes: 6 additions & 0 deletions cli/cmd/project/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type DeployOpts struct {
ProdVersion string
PrimaryBranch string
Slots int
DevSlots int
PushEnv bool

ArchiveUpload bool
Expand Down Expand Up @@ -286,11 +287,15 @@ func DeployCmd(ch *cmdutil.Helper) *cobra.Command {
deployCmd.Flags().StringVar(&opts.Provisioner, "provisioner", "", "Project provisioner")
deployCmd.Flags().StringVar(&opts.PrimaryBranch, "primary-branch", "", "Git branch to deploy from (default: the default Git branch)")
deployCmd.Flags().IntVar(&opts.Slots, "prod-slots", local.DefaultProdSlots(ch), "Slots to allocate for production deployments")
deployCmd.Flags().IntVar(&opts.DevSlots, "dev-slots", local.DefaultDevSlots(ch), "Slots to allocate for dev deployments")
deployCmd.Flags().BoolVar(&opts.PushEnv, "push-env", true, "Push local .env file to Rill Cloud")
if !ch.IsDev() {
if err := deployCmd.Flags().MarkHidden("prod-slots"); err != nil {
panic(err)
}
if err := deployCmd.Flags().MarkHidden("dev-slots"); err != nil {
panic(err)
}
}

deployCmd.Flags().BoolVar(&opts.SkipDeploy, "skip-deploy", false, "Skip the runtime deployment step (for testing only)")
Expand Down Expand Up @@ -381,6 +386,7 @@ func DeployWithUploadFlow(ctx context.Context, ch *cmdutil.Helper, opts *DeployO
Provisioner: opts.Provisioner,
ProdVersion: opts.ProdVersion,
ProdSlots: int64(opts.Slots),
DevSlots: int64(opts.DevSlots),
Public: opts.Public,
DirectoryName: filepath.Base(localProjectPath),
SkipDeploy: opts.SkipDeploy,
Expand Down
19 changes: 19 additions & 0 deletions cli/cmd/project/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command {
var name, description, primaryBranch, subpath, path, provisioner, gitRemote string
var public bool
var prodTTL int64
var prodSlots, devSlots int

editCmd := &cobra.Command{
Use: "edit [<project-name>]",
Expand Down Expand Up @@ -66,6 +67,22 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command {
flagSet = true
req.GitRemote = &gitRemote
}
if cmd.Flags().Changed("prod-slots") {
if prodSlots <= 0 {
return fmt.Errorf("--prod-slots must be greater than zero")
}
flagSet = true
prodSlotsInt64 := int64(prodSlots)
req.ProdSlots = &prodSlotsInt64
}
if cmd.Flags().Changed("dev-slots") {
if devSlots <= 0 {
return fmt.Errorf("--dev-slots must be greater than zero")
}
flagSet = true
devSlotsInt64 := int64(devSlots)
req.DevSlots = &devSlotsInt64
}

if !flagSet {
return fmt.Errorf("must specify at least one update flag")
Expand Down Expand Up @@ -93,6 +110,8 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command {
editCmd.Flags().StringVar(&subpath, "subpath", "", "Relative path to project in the repository (for monorepos)")
editCmd.Flags().StringVar(&provisioner, "provisioner", "", "Project provisioner (default: current provisioner)")
editCmd.Flags().Int64Var(&prodTTL, "prod-ttl-seconds", 0, "Time-to-live in seconds for production deployment (0 means no expiration)")
editCmd.Flags().IntVar(&prodSlots, "prod-slots", 0, "Slots to allocate for production deployments")
editCmd.Flags().IntVar(&devSlots, "dev-slots", 0, "Slots to allocate for dev deployments")

return editCmd
}
11 changes: 10 additions & 1 deletion cli/cmd/sudo/project/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func EditCmd(ch *cmdutil.Helper) *cobra.Command {
var prodSlots int
var prodSlots, devSlots int
var prodVersion string

editCmd := &cobra.Command{
Expand Down Expand Up @@ -38,6 +38,14 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command {
req.ProdVersion = &prodVersion
isEditRequested = true
}
if cmd.Flags().Changed("dev-slots") {
if devSlots <= 0 {
return fmt.Errorf("--dev-slots must be greater than zero")
}
devSlotsInt64 := int64(devSlots)
req.DevSlots = &devSlotsInt64
isEditRequested = true
}

if !isEditRequested {
ch.Printf("No edit requested\n")
Expand All @@ -62,6 +70,7 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command {
}

editCmd.Flags().IntVar(&prodSlots, "prod-slots", 0, "Slots to allocate for production deployments")
editCmd.Flags().IntVar(&devSlots, "dev-slots", 0, "Slots to allocate for dev deployments")
editCmd.Flags().StringVar(&prodVersion, "prod-version", "", "Rill version for production deployment")
return editCmd
}
3 changes: 3 additions & 0 deletions cli/pkg/local/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ func (s *Server) DeployProject(ctx context.Context, r *connect.Request[localv1.D
Provisioner: "",
ProdVersion: "",
ProdSlots: int64(DefaultProdSlots(s.app.ch)),
DevSlots: int64(DefaultDevSlots(s.app.ch)),
Public: false,
DirectoryName: directoryName,
ArchiveAssetId: assetID,
Expand All @@ -371,6 +372,7 @@ func (s *Server) DeployProject(ctx context.Context, r *connect.Request[localv1.D
Provisioner: "",
ProdVersion: "",
ProdSlots: int64(DefaultProdSlots(s.app.ch)),
DevSlots: int64(DefaultDevSlots(s.app.ch)),
Public: false,
DirectoryName: directoryName,
GitRemote: ghRepo.Remote,
Expand Down Expand Up @@ -431,6 +433,7 @@ func (s *Server) DeployProject(ctx context.Context, r *connect.Request[localv1.D
Provisioner: "",
ProdVersion: "",
ProdSlots: int64(DefaultProdSlots(s.app.ch)),
DevSlots: int64(DefaultDevSlots(s.app.ch)),
Public: false,
DirectoryName: directoryName,
GitRemote: githubRemote,
Expand Down
13 changes: 13 additions & 0 deletions cli/pkg/local/slots.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,16 @@ func DefaultProdSlots(ch *cmdutil.Helper) int {
}
return 4
}

// DefaultDevSlots returns the default number of slots for dev environments.
//
// 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 8
}
2 changes: 2 additions & 0 deletions docs/docs/reference/cli/project/edit.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ rill project edit [<project-name>] [flags]
--subpath string Relative path to project in the repository (for monorepos)
--provisioner string Project provisioner (default: current provisioner)
--prod-ttl-seconds int Time-to-live in seconds for production deployment (0 means no expiration)
--prod-slots int Slots to allocate for production deployments
--dev-slots int Slots to allocate for dev deployments
```

### Global flags
Expand Down
6 changes: 6 additions & 0 deletions proto/gen/rill/admin/v1/admin.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,9 @@ paths:
description: archive_asset_id is set for projects whose project files are not stored in github but are managed by rill.
prodVersion:
type: string
devSlots:
type: string
format: int64
skipDeploy:
type: boolean
x-visibility: public
Expand Down Expand Up @@ -1196,6 +1199,9 @@ paths:
format: int64
prodVersion:
type: string
devSlots:
type: string
format: int64
superuserForceAccess:
type: boolean
x-visibility: public
Expand Down
Loading
Loading