312 lines
7.1 KiB
Go
312 lines
7.1 KiB
Go
package vm
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
type SQLRepository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewSQLRepository(db *sql.DB) SQLRepository {
|
|
return SQLRepository{db: db}
|
|
}
|
|
|
|
func (r SQLRepository) ListProjectVMs(ctx context.Context, profileID string, projectID string) ([]VM, bool, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
select
|
|
v.id::text,
|
|
v.project_id::text,
|
|
p.tenant_id::text,
|
|
v.cluster_id::text,
|
|
v.proxmox_vmid,
|
|
v.node,
|
|
v.name,
|
|
v.status::text,
|
|
v.vcpu,
|
|
v.ram_mb,
|
|
v.disk_gb,
|
|
v.created_at,
|
|
v.updated_at,
|
|
p.name,
|
|
m.role::text
|
|
from public.projects p
|
|
join public.memberships m
|
|
on m.tenant_id = p.tenant_id
|
|
and m.profile_id = $1
|
|
left join public.vms v
|
|
on v.project_id = p.id
|
|
where p.id = $2
|
|
order by v.created_at desc nulls last, v.name asc
|
|
`, profileID, projectID)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var vms []VM
|
|
foundProject := false
|
|
for rows.Next() {
|
|
var record nullableVMRecord
|
|
if err := rows.Scan(
|
|
&record.ID,
|
|
&record.ProjectID,
|
|
&record.TenantID,
|
|
&record.ClusterID,
|
|
&record.ProxmoxVMID,
|
|
&record.Node,
|
|
&record.Name,
|
|
&record.Status,
|
|
&record.VCPU,
|
|
&record.RAMMB,
|
|
&record.DiskGB,
|
|
&record.CreatedAt,
|
|
&record.UpdatedAt,
|
|
&record.ProjectName,
|
|
&record.MembershipRole,
|
|
); err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
foundProject = true
|
|
if !record.ID.Valid {
|
|
continue
|
|
}
|
|
vms = append(vms, record.vm())
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
return vms, foundProject, nil
|
|
}
|
|
|
|
func (r SQLRepository) GetVM(ctx context.Context, profileID string, vmID string) (VM, bool, error) {
|
|
var vm VM
|
|
err := r.db.QueryRowContext(ctx, `
|
|
select
|
|
v.id::text,
|
|
v.project_id::text,
|
|
p.tenant_id::text,
|
|
v.cluster_id::text,
|
|
v.proxmox_vmid,
|
|
v.node,
|
|
v.name,
|
|
v.status::text,
|
|
v.vcpu,
|
|
v.ram_mb,
|
|
v.disk_gb,
|
|
v.created_at,
|
|
v.updated_at,
|
|
p.name,
|
|
m.role::text
|
|
from public.vms v
|
|
join public.projects p
|
|
on p.id = v.project_id
|
|
join public.memberships m
|
|
on m.tenant_id = p.tenant_id
|
|
and m.profile_id = $1
|
|
where v.id = $2
|
|
`, profileID, vmID).Scan(
|
|
&vm.ID,
|
|
&vm.ProjectID,
|
|
&vm.TenantID,
|
|
&vm.ClusterID,
|
|
&vm.ProxmoxVMID,
|
|
&vm.Node,
|
|
&vm.Name,
|
|
&vm.Status,
|
|
&vm.VCPU,
|
|
&vm.RAMMB,
|
|
&vm.DiskGB,
|
|
&vm.CreatedAt,
|
|
&vm.UpdatedAt,
|
|
&vm.ProjectName,
|
|
&vm.MembershipRole,
|
|
)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return VM{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return VM{}, false, err
|
|
}
|
|
|
|
return vm, true, nil
|
|
}
|
|
|
|
type nullableVMRecord struct {
|
|
ID sql.NullString
|
|
ProjectID sql.NullString
|
|
TenantID sql.NullString
|
|
ClusterID sql.NullString
|
|
ProxmoxVMID sql.NullInt64
|
|
Node sql.NullString
|
|
Name sql.NullString
|
|
Status sql.NullString
|
|
VCPU sql.NullInt64
|
|
RAMMB sql.NullInt64
|
|
DiskGB sql.NullInt64
|
|
CreatedAt sql.NullTime
|
|
UpdatedAt sql.NullTime
|
|
ProjectName string
|
|
MembershipRole string
|
|
}
|
|
|
|
func (r nullableVMRecord) vm() VM {
|
|
return VM{
|
|
ID: r.ID.String,
|
|
ProjectID: r.ProjectID.String,
|
|
TenantID: r.TenantID.String,
|
|
ClusterID: r.ClusterID.String,
|
|
ProxmoxVMID: int(r.ProxmoxVMID.Int64),
|
|
Node: r.Node.String,
|
|
Name: r.Name.String,
|
|
Status: r.Status.String,
|
|
VCPU: int(r.VCPU.Int64),
|
|
RAMMB: int(r.RAMMB.Int64),
|
|
DiskGB: int(r.DiskGB.Int64),
|
|
CreatedAt: r.CreatedAt.Time,
|
|
UpdatedAt: r.UpdatedAt.Time,
|
|
ProjectName: r.ProjectName,
|
|
MembershipRole: r.MembershipRole,
|
|
}
|
|
}
|
|
|
|
type ProjectInfo struct {
|
|
TenantID string
|
|
Name string
|
|
Role string
|
|
}
|
|
|
|
type QuotaInfo struct {
|
|
UsedVMs int
|
|
UsedVCPU int
|
|
UsedRAMMB int
|
|
UsedDiskGB int
|
|
MaxVMs int
|
|
MaxVCPU int
|
|
MaxRAMMB int
|
|
MaxDiskGB int
|
|
}
|
|
|
|
func (r SQLRepository) GetProjectInfo(ctx context.Context, profileID string, projectID string) (ProjectInfo, bool, error) {
|
|
var info ProjectInfo
|
|
err := r.db.QueryRowContext(ctx, `
|
|
select
|
|
p.tenant_id::text,
|
|
p.name,
|
|
m.role::text
|
|
from public.projects p
|
|
join public.memberships m
|
|
on m.tenant_id = p.tenant_id
|
|
and m.profile_id = $1
|
|
where p.id = $2
|
|
`, profileID, projectID).Scan(&info.TenantID, &info.Name, &info.Role)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return ProjectInfo{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return ProjectInfo{}, false, err
|
|
}
|
|
|
|
return info, true, nil
|
|
}
|
|
|
|
func (r SQLRepository) CheckQuota(ctx context.Context, projectID string) (QuotaInfo, error) {
|
|
var q QuotaInfo
|
|
err := r.db.QueryRowContext(ctx, `
|
|
select
|
|
coalesce(sum(case when v.status <> 'failed' and v.status <> 'deleting' and v.status <> 'deleted' then 1 else 0 end), 0) as used_vms,
|
|
coalesce(sum(case when v.status <> 'failed' and v.status <> 'deleting' and v.status <> 'deleted' then v.vcpu else 0 end), 0) as used_vcpu,
|
|
coalesce(sum(case when v.status <> 'failed' and v.status <> 'deleting' and v.status <> 'deleted' then v.ram_mb else 0 end), 0) as used_ram_mb,
|
|
coalesce(sum(case when v.status <> 'failed' and v.status <> 'deleting' and v.status <> 'deleted' then v.disk_gb else 0 end), 0) as used_disk_gb,
|
|
coalesce(pq.max_vms, 10),
|
|
coalesce(pq.max_vcpu, 8),
|
|
coalesce(pq.max_ram_mb, 16384),
|
|
coalesce(pq.max_disk_gb, 200)
|
|
from public.projects p
|
|
left join public.vms v on v.project_id = p.id
|
|
left join public.project_quotas pq on pq.project_id = p.id
|
|
where p.id = $1
|
|
group by pq.max_vms, pq.max_vcpu, pq.max_ram_mb, pq.max_disk_gb
|
|
`, projectID).Scan(&q.UsedVMs, &q.UsedVCPU, &q.UsedRAMMB, &q.UsedDiskGB, &q.MaxVMs, &q.MaxVCPU, &q.MaxRAMMB, &q.MaxDiskGB)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
q.MaxVMs = 10
|
|
q.MaxVCPU = 8
|
|
q.MaxRAMMB = 16384
|
|
q.MaxDiskGB = 200
|
|
return q, nil
|
|
}
|
|
if err != nil {
|
|
return QuotaInfo{}, err
|
|
}
|
|
|
|
return q, nil
|
|
}
|
|
|
|
func (r SQLRepository) ReserveNextVMID(ctx context.Context, clusterID string) (int, error) {
|
|
var vmid int
|
|
err := r.db.QueryRowContext(ctx, `
|
|
select public.reserve_next_vmid($1::uuid)
|
|
`, clusterID).Scan(&vmid)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("reserve vmid: %w", err)
|
|
}
|
|
|
|
return vmid, nil
|
|
}
|
|
|
|
func (r SQLRepository) InsertVM(ctx context.Context, vm InsertVMRecord) (VM, error) {
|
|
var result VM
|
|
err := r.db.QueryRowContext(ctx, `
|
|
insert into public.vms (project_id, cluster_id, proxmox_vmid, node, name, status, vcpu, ram_mb, disk_gb)
|
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
returning
|
|
id::text,
|
|
project_id::text,
|
|
cluster_id::text,
|
|
proxmox_vmid,
|
|
node,
|
|
name,
|
|
status::text,
|
|
vcpu,
|
|
ram_mb,
|
|
disk_gb,
|
|
created_at,
|
|
updated_at
|
|
`, vm.ProjectID, vm.ClusterID, vm.ProxmoxVMID, vm.Node, vm.Name, vm.Status, vm.VCPU, vm.RAMMB, vm.DiskGB).Scan(
|
|
&result.ID,
|
|
&result.ProjectID,
|
|
&result.ClusterID,
|
|
&result.ProxmoxVMID,
|
|
&result.Node,
|
|
&result.Name,
|
|
&result.Status,
|
|
&result.VCPU,
|
|
&result.RAMMB,
|
|
&result.DiskGB,
|
|
&result.CreatedAt,
|
|
&result.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return VM{}, fmt.Errorf("insert vm: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
type InsertVMRecord struct {
|
|
ProjectID string
|
|
ClusterID string
|
|
ProxmoxVMID int
|
|
Node string
|
|
Name string
|
|
Status string
|
|
VCPU int
|
|
RAMMB int
|
|
DiskGB int
|
|
}
|