2022-05-05 13:25:50 +00:00
// Copyright 2022 Cloudbase Solutions SRL
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
2022-04-25 00:03:26 +00:00
package sql
import (
"context"
2023-06-28 21:52:50 +00:00
"fmt"
2024-01-05 23:32:16 +00:00
"log/slog"
2024-04-15 08:32:19 +00:00
"net/url"
2023-06-28 21:52:50 +00:00
"strings"
2022-04-25 00:03:26 +00:00
"github.com/pkg/errors"
2022-07-07 16:48:00 +00:00
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
2022-04-25 00:03:26 +00:00
"gorm.io/gorm"
2022-09-06 21:35:01 +03:00
"gorm.io/gorm/logger"
2022-07-07 16:48:00 +00:00
2024-04-15 08:32:19 +00:00
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
"github.com/cloudbase/garm/auth"
2023-03-12 16:01:49 +02:00
"github.com/cloudbase/garm/config"
"github.com/cloudbase/garm/database/common"
2024-04-03 14:46:32 +00:00
"github.com/cloudbase/garm/database/watcher"
2024-04-15 08:32:19 +00:00
"github.com/cloudbase/garm/params"
"github.com/cloudbase/garm/util/appdefaults"
2022-04-25 00:03:26 +00:00
)
2025-04-27 20:28:06 +00:00
const (
repositoryFieldName string = "Repository"
organizationFieldName string = "Organization"
enterpriseFieldName string = "Enterprise"
)
2022-07-07 16:48:00 +00:00
// newDBConn returns a new gorm db connection, given the config
func newDBConn ( dbCfg config . Database ) ( conn * gorm . DB , err error ) {
dbType , connURI , err := dbCfg . GormParams ( )
if err != nil {
return nil , errors . Wrap ( err , "getting DB URI string" )
}
2022-09-06 21:35:01 +03:00
gormConfig := & gorm . Config { }
if ! dbCfg . Debug {
gormConfig . Logger = logger . Default . LogMode ( logger . Silent )
}
2022-07-07 16:48:00 +00:00
switch dbType {
case config . MySQLBackend :
2022-09-06 21:35:01 +03:00
conn , err = gorm . Open ( mysql . Open ( connURI ) , gormConfig )
2022-07-07 16:48:00 +00:00
case config . SQLiteBackend :
2022-09-06 21:35:01 +03:00
conn , err = gorm . Open ( sqlite . Open ( connURI ) , gormConfig )
2022-07-07 16:48:00 +00:00
}
if err != nil {
return nil , errors . Wrap ( err , "connecting to database" )
}
if dbCfg . Debug {
conn = conn . Debug ( )
}
return conn , nil
}
2022-04-25 00:03:26 +00:00
func NewSQLDatabase ( ctx context . Context , cfg config . Database ) ( common . Store , error ) {
2022-07-07 16:48:00 +00:00
conn , err := newDBConn ( cfg )
2022-04-25 00:03:26 +00:00
if err != nil {
return nil , errors . Wrap ( err , "creating DB connection" )
}
2024-06-17 19:42:50 +00:00
producer , err := watcher . RegisterProducer ( ctx , "sql" )
2024-04-03 14:46:32 +00:00
if err != nil {
return nil , errors . Wrap ( err , "registering producer" )
}
2022-04-25 00:03:26 +00:00
db := & sqlDatabase {
2024-04-03 14:46:32 +00:00
conn : conn ,
ctx : ctx ,
cfg : cfg ,
producer : producer ,
2022-04-25 00:03:26 +00:00
}
if err := db . migrateDB ( ) ; err != nil {
return nil , errors . Wrap ( err , "migrating database" )
}
return db , nil
}
type sqlDatabase struct {
2024-04-03 14:46:32 +00:00
conn * gorm . DB
ctx context . Context
cfg config . Database
producer common . Producer
2022-04-25 00:03:26 +00:00
}
2023-06-28 21:52:50 +00:00
var renameTemplate = `
PRAGMA foreign_keys = OFF ;
BEGIN TRANSACTION ;
ALTER TABLE % s RENAME TO % s_old ;
COMMIT ;
`
var restoreNameTemplate = `
PRAGMA foreign_keys = OFF ;
BEGIN TRANSACTION ;
DROP TABLE IF EXISTS % s ;
ALTER TABLE % s_old RENAME TO % s ;
COMMIT ;
`
var copyContentsTemplate = `
PRAGMA foreign_keys = OFF ;
BEGIN TRANSACTION ;
INSERT INTO % s SELECT * FROM % s_old ;
DROP TABLE % s_old ;
COMMIT ;
`
func ( s * sqlDatabase ) cascadeMigrationSQLite ( model interface { } , name string , justDrop bool ) error {
if ! s . conn . Migrator ( ) . HasTable ( name ) {
return nil
}
defer s . conn . Exec ( "PRAGMA foreign_keys = ON;" )
var data string
var indexes [ ] string
2023-06-29 06:22:15 +00:00
if err := s . conn . Raw ( fmt . Sprintf ( "select sql from sqlite_master where type='table' and tbl_name='%s'" , name ) ) . Scan ( & data ) . Error ; err != nil {
2023-06-28 21:52:50 +00:00
if ! errors . Is ( err , gorm . ErrRecordNotFound ) {
return fmt . Errorf ( "failed to get table %s: %w" , name , err )
}
}
if err := s . conn . Raw ( fmt . Sprintf ( "SELECT name FROM sqlite_master WHERE type == 'index' AND tbl_name == '%s' and name not like 'sqlite_%%'" , name ) ) . Scan ( & indexes ) . Error ; err != nil {
if ! errors . Is ( err , gorm . ErrRecordNotFound ) {
return fmt . Errorf ( "failed to get table indexes %s: %w" , name , err )
}
}
2023-07-05 19:49:48 +00:00
if strings . Contains ( data , "ON DELETE" ) {
2023-06-28 21:52:50 +00:00
return nil
}
if justDrop {
if err := s . conn . Migrator ( ) . DropTable ( model ) ; err != nil {
return fmt . Errorf ( "failed to drop table %s: %w" , name , err )
}
return nil
}
for _ , index := range indexes {
if err := s . conn . Migrator ( ) . DropIndex ( model , index ) ; err != nil {
return fmt . Errorf ( "failed to drop index %s: %w" , index , err )
}
}
err := s . conn . Exec ( fmt . Sprintf ( renameTemplate , name , name ) ) . Error
if err != nil {
return fmt . Errorf ( "failed to rename table %s: %w" , name , err )
}
if model != nil {
if err := s . conn . Migrator ( ) . AutoMigrate ( model ) ; err != nil {
if err := s . conn . Exec ( fmt . Sprintf ( restoreNameTemplate , name , name , name ) ) . Error ; err != nil {
2024-01-05 23:32:16 +00:00
slog . With ( slog . Any ( "error" , err ) ) . Error ( "failed to restore table" , "table" , name )
2023-06-28 21:52:50 +00:00
}
return fmt . Errorf ( "failed to create table %s: %w" , name , err )
}
}
err = s . conn . Exec ( fmt . Sprintf ( copyContentsTemplate , name , name , name ) ) . Error
if err != nil {
return fmt . Errorf ( "failed to copy contents to table %s: %w" , name , err )
}
return nil
}
func ( s * sqlDatabase ) cascadeMigration ( ) error {
switch s . cfg . DbBackend {
case config . SQLiteBackend :
if err := s . cascadeMigrationSQLite ( & Address { } , "addresses" , true ) ; err != nil {
return fmt . Errorf ( "failed to drop table addresses: %w" , err )
}
if err := s . cascadeMigrationSQLite ( & InstanceStatusUpdate { } , "instance_status_updates" , true ) ; err != nil {
return fmt . Errorf ( "failed to drop table instance_status_updates: %w" , err )
}
if err := s . cascadeMigrationSQLite ( & Tag { } , "pool_tags" , false ) ; err != nil {
return fmt . Errorf ( "failed to migrate addresses: %w" , err )
}
2023-07-05 19:49:48 +00:00
if err := s . cascadeMigrationSQLite ( & WorkflowJob { } , "workflow_jobs" , false ) ; err != nil {
return fmt . Errorf ( "failed to migrate addresses: %w" , err )
}
2023-06-28 21:52:50 +00:00
case config . MySQLBackend :
return nil
default :
return fmt . Errorf ( "invalid db backend: %s" , s . cfg . DbBackend )
}
return nil
}
2024-04-19 10:04:02 +00:00
func ( s * sqlDatabase ) ensureGithubEndpoint ( ) error {
// Create the default Github endpoint.
createEndpointParams := params . CreateGithubEndpointParams {
Name : "github.com" ,
Description : "The github.com endpoint" ,
APIBaseURL : appdefaults . GithubDefaultBaseURL ,
BaseURL : appdefaults . DefaultGithubURL ,
UploadBaseURL : appdefaults . GithubDefaultUploadBaseURL ,
}
2025-05-24 21:10:05 +00:00
var epCount int64
if err := s . conn . Model ( & GithubEndpoint { } ) . Count ( & epCount ) . Error ; err != nil {
if ! errors . Is ( err , gorm . ErrRecordNotFound ) {
return errors . Wrap ( err , "counting github endpoints" )
}
}
if epCount == 0 {
if _ , err := s . CreateGithubEndpoint ( context . Background ( ) , createEndpointParams ) ; err != nil {
if ! errors . Is ( err , runnerErrors . ErrDuplicateEntity ) {
return errors . Wrap ( err , "creating default github endpoint" )
}
2024-04-19 10:04:02 +00:00
}
}
return nil
}
2024-04-15 08:32:19 +00:00
func ( s * sqlDatabase ) migrateCredentialsToDB ( ) ( err error ) {
s . conn . Exec ( "PRAGMA foreign_keys = OFF" )
defer s . conn . Exec ( "PRAGMA foreign_keys = ON" )
adminUser , err := s . GetAdminUser ( s . ctx )
if err != nil {
if errors . Is ( err , runnerErrors . ErrNotFound ) {
// Admin user doesn't exist. This is a new deploy. Nothing to migrate.
return nil
}
return errors . Wrap ( err , "getting admin user" )
}
// Impersonate the admin user. We're migrating from config credentials to
// database credentials. At this point, there is no other user than the admin
// user. GARM is not yet multi-user, so it's safe to assume we only have this
// one user.
adminCtx := context . Background ( )
2024-07-02 22:26:12 +00:00
adminCtx = auth . PopulateContext ( adminCtx , adminUser , nil )
2024-04-15 08:32:19 +00:00
slog . Info ( "migrating credentials to DB" )
slog . Info ( "creating github endpoints table" )
if err := s . conn . AutoMigrate ( & GithubEndpoint { } ) ; err != nil {
return errors . Wrap ( err , "migrating github endpoints" )
}
defer func ( ) {
if err != nil {
slog . With ( slog . Any ( "error" , err ) ) . Error ( "rolling back github github endpoints table" )
s . conn . Migrator ( ) . DropTable ( & GithubEndpoint { } )
}
} ( )
slog . Info ( "creating github credentials table" )
if err := s . conn . AutoMigrate ( & GithubCredentials { } ) ; err != nil {
return errors . Wrap ( err , "migrating github credentials" )
}
defer func ( ) {
if err != nil {
slog . With ( slog . Any ( "error" , err ) ) . Error ( "rolling back github github credentials table" )
s . conn . Migrator ( ) . DropTable ( & GithubCredentials { } )
}
} ( )
// Nothing to migrate.
if len ( s . cfg . MigrateCredentials ) == 0 {
return nil
}
slog . Info ( "importing credentials from config" )
for _ , cred := range s . cfg . MigrateCredentials {
slog . Info ( "importing credential" , "name" , cred . Name )
parsed , err := url . Parse ( cred . BaseEndpoint ( ) )
if err != nil {
return errors . Wrap ( err , "parsing base URL" )
}
certBundle , err := cred . CACertBundle ( )
if err != nil {
return errors . Wrap ( err , "getting CA cert bundle" )
}
hostname := parsed . Hostname ( )
createParams := params . CreateGithubEndpointParams {
Name : hostname ,
Description : fmt . Sprintf ( "Endpoint for %s" , hostname ) ,
APIBaseURL : cred . APIEndpoint ( ) ,
BaseURL : cred . BaseEndpoint ( ) ,
UploadBaseURL : cred . UploadEndpoint ( ) ,
CACertBundle : certBundle ,
}
2025-05-12 21:47:13 +00:00
var endpoint params . ForgeEndpoint
2024-04-15 08:32:19 +00:00
endpoint , err = s . GetGithubEndpoint ( adminCtx , hostname )
if err != nil {
if ! errors . Is ( err , runnerErrors . ErrNotFound ) {
return errors . Wrap ( err , "getting github endpoint" )
}
endpoint , err = s . CreateGithubEndpoint ( adminCtx , createParams )
if err != nil {
return errors . Wrap ( err , "creating default github endpoint" )
}
}
credParams := params . CreateGithubCredentialsParams {
Name : cred . Name ,
Description : cred . Description ,
2024-04-19 08:47:44 +00:00
Endpoint : endpoint . Name ,
2025-05-12 21:47:13 +00:00
AuthType : params . ForgeAuthType ( cred . GetAuthType ( ) ) ,
2024-04-15 08:32:19 +00:00
}
switch credParams . AuthType {
2025-05-12 21:47:13 +00:00
case params . ForgeAuthTypeApp :
2024-04-15 08:32:19 +00:00
keyBytes , err := cred . App . PrivateKeyBytes ( )
if err != nil {
return errors . Wrap ( err , "getting private key bytes" )
}
credParams . App = params . GithubApp {
AppID : cred . App . AppID ,
InstallationID : cred . App . InstallationID ,
PrivateKeyBytes : keyBytes ,
}
if err := credParams . App . Validate ( ) ; err != nil {
return errors . Wrap ( err , "validating app credentials" )
}
2025-05-12 21:47:13 +00:00
case params . ForgeAuthTypePAT :
2024-04-17 13:45:56 +00:00
token := cred . PAT . OAuth2Token
if token == "" {
token = cred . OAuth2Token
}
if token == "" {
2024-04-15 08:32:19 +00:00
return errors . New ( "missing OAuth2 token" )
}
credParams . PAT = params . GithubPAT {
2024-04-17 13:45:56 +00:00
OAuth2Token : token ,
2024-04-15 08:32:19 +00:00
}
}
2024-04-19 08:47:44 +00:00
creds , err := s . CreateGithubCredentials ( adminCtx , credParams )
2024-04-15 08:32:19 +00:00
if err != nil {
return errors . Wrap ( err , "creating github credentials" )
}
2024-05-07 13:13:16 +03:00
if err := s . conn . Exec ( "update repositories set credentials_id = ?,endpoint_name = ? where credentials_name = ?" , creds . ID , creds . Endpoint . Name , creds . Name ) . Error ; err != nil {
2024-04-15 08:32:19 +00:00
return errors . Wrap ( err , "updating repositories" )
}
2024-05-07 13:13:16 +03:00
if err := s . conn . Exec ( "update organizations set credentials_id = ?,endpoint_name = ? where credentials_name = ?" , creds . ID , creds . Endpoint . Name , creds . Name ) . Error ; err != nil {
2024-04-15 08:32:19 +00:00
return errors . Wrap ( err , "updating organizations" )
}
2024-05-07 13:13:16 +03:00
if err := s . conn . Exec ( "update enterprises set credentials_id = ?,endpoint_name = ? where credentials_name = ?" , creds . ID , creds . Endpoint . Name , creds . Name ) . Error ; err != nil {
2024-04-15 08:32:19 +00:00
return errors . Wrap ( err , "updating enterprises" )
}
}
return nil
}
2025-07-18 07:51:50 +00:00
func ( s * sqlDatabase ) migrateWorkflow ( ) error {
if s . conn . Migrator ( ) . HasTable ( & WorkflowJob { } ) {
if s . conn . Migrator ( ) . HasColumn ( & WorkflowJob { } , "runner_name" ) {
// Remove jobs that are not in "queued" status. We really only care about queued jobs. Once they transition
// to something else, we don't really consume them anyway.
if err := s . conn . Exec ( "delete from workflow_jobs where status is not 'queued'" ) . Error ; err != nil {
return errors . Wrap ( err , "updating workflow_jobs" )
}
if err := s . conn . Migrator ( ) . DropColumn ( & WorkflowJob { } , "runner_name" ) ; err != nil {
return errors . Wrap ( err , "updating workflow_jobs" )
}
}
}
return nil
}
2022-04-25 00:03:26 +00:00
func ( s * sqlDatabase ) migrateDB ( ) error {
2022-10-05 17:12:45 +03:00
if s . conn . Migrator ( ) . HasIndex ( & Organization { } , "idx_organizations_name" ) {
if err := s . conn . Migrator ( ) . DropIndex ( & Organization { } , "idx_organizations_name" ) ; err != nil {
2024-01-05 23:32:16 +00:00
slog . With ( slog . Any ( "error" , err ) ) . Error ( "failed to drop index idx_organizations_name" )
2022-10-05 17:12:45 +03:00
}
}
if s . conn . Migrator ( ) . HasIndex ( & Repository { } , "idx_owner" ) {
if err := s . conn . Migrator ( ) . DropIndex ( & Repository { } , "idx_owner" ) ; err != nil {
2024-01-05 23:32:16 +00:00
slog . With ( slog . Any ( "error" , err ) ) . Error ( "failed to drop index idx_owner" )
2022-10-05 17:12:45 +03:00
}
}
2023-06-28 21:52:50 +00:00
if err := s . cascadeMigration ( ) ; err != nil {
return errors . Wrap ( err , "running cascade migration" )
}
2023-06-28 22:57:03 +00:00
if s . conn . Migrator ( ) . HasTable ( & Pool { } ) {
if err := s . conn . Exec ( "update pools set repo_id=NULL where repo_id='00000000-0000-0000-0000-000000000000'" ) . Error ; err != nil {
return errors . Wrap ( err , "updating pools" )
}
if err := s . conn . Exec ( "update pools set org_id=NULL where org_id='00000000-0000-0000-0000-000000000000'" ) . Error ; err != nil {
return errors . Wrap ( err , "updating pools" )
}
if err := s . conn . Exec ( "update pools set enterprise_id=NULL where enterprise_id='00000000-0000-0000-0000-000000000000'" ) . Error ; err != nil {
return errors . Wrap ( err , "updating pools" )
}
}
2025-07-18 07:51:50 +00:00
if err := s . migrateWorkflow ( ) ; err != nil {
return errors . Wrap ( err , "migrating workflows" )
2023-08-26 19:43:57 +00:00
}
2025-05-12 17:32:37 +00:00
if s . conn . Migrator ( ) . HasTable ( & GithubEndpoint { } ) {
if ! s . conn . Migrator ( ) . HasColumn ( & GithubEndpoint { } , "endpoint_type" ) {
if err := s . conn . Migrator ( ) . AutoMigrate ( & GithubEndpoint { } ) ; err != nil {
return errors . Wrap ( err , "migrating github endpoints" )
}
if err := s . conn . Exec ( "update github_endpoints set endpoint_type = 'github' where endpoint_type is null" ) . Error ; err != nil {
return errors . Wrap ( err , "updating github endpoints" )
}
}
}
2024-04-15 08:32:19 +00:00
var needsCredentialMigration bool
if ! s . conn . Migrator ( ) . HasTable ( & GithubCredentials { } ) || ! s . conn . Migrator ( ) . HasTable ( & GithubEndpoint { } ) {
needsCredentialMigration = true
}
2024-07-01 10:27:31 +00:00
var hasMinAgeField bool
if s . conn . Migrator ( ) . HasTable ( & ControllerInfo { } ) && s . conn . Migrator ( ) . HasColumn ( & ControllerInfo { } , "minimum_job_age_backoff" ) {
hasMinAgeField = true
}
2024-04-15 08:32:19 +00:00
s . conn . Exec ( "PRAGMA foreign_keys = OFF" )
2022-04-25 00:03:26 +00:00
if err := s . conn . AutoMigrate (
2024-04-15 08:32:19 +00:00
& User { } ,
& GithubEndpoint { } ,
& GithubCredentials { } ,
2025-05-14 00:34:54 +00:00
& GiteaCredentials { } ,
2022-04-25 00:03:26 +00:00
& Tag { } ,
& Pool { } ,
& Repository { } ,
& Organization { } ,
2022-10-13 16:09:28 +00:00
& Enterprise { } ,
2025-04-16 16:39:16 +00:00
& EnterpriseEvent { } ,
& OrganizationEvent { } ,
& RepositoryEvent { } ,
2022-04-26 20:29:58 +00:00
& Address { } ,
2022-05-03 19:49:14 +00:00
& InstanceStatusUpdate { } ,
2022-04-26 20:29:58 +00:00
& Instance { } ,
2022-04-28 16:13:20 +00:00
& ControllerInfo { } ,
2023-04-10 00:03:49 +00:00
& WorkflowJob { } ,
2025-04-08 09:15:54 +00:00
& ScaleSet { } ,
2022-04-25 00:03:26 +00:00
) ; err != nil {
2022-10-13 16:09:28 +00:00
return errors . Wrap ( err , "running auto migrate" )
2022-04-25 00:03:26 +00:00
}
2024-04-15 08:32:19 +00:00
s . conn . Exec ( "PRAGMA foreign_keys = ON" )
2022-04-25 00:03:26 +00:00
2024-07-01 10:27:31 +00:00
if ! hasMinAgeField {
var controller ControllerInfo
if err := s . conn . First ( & controller ) . Error ; err != nil {
if ! errors . Is ( err , gorm . ErrRecordNotFound ) {
return errors . Wrap ( err , "updating controller info" )
}
} else {
controller . MinimumJobAgeBackoff = 30
if err := s . conn . Save ( & controller ) . Error ; err != nil {
return errors . Wrap ( err , "updating controller info" )
}
}
}
2024-04-19 10:04:02 +00:00
if err := s . ensureGithubEndpoint ( ) ; err != nil {
return errors . Wrap ( err , "ensuring github endpoint" )
}
2024-04-15 08:32:19 +00:00
if needsCredentialMigration {
if err := s . migrateCredentialsToDB ( ) ; err != nil {
return errors . Wrap ( err , "migrating credentials" )
}
}
2022-04-25 00:03:26 +00:00
return nil
}