garm/database/sql/gitea.go
Gabriel Adrian Samfira 87055f23da Make the default github endpoint mutable
This change allows users to remove the default github endpoint
if no credentials are set on it.

A new protection is added on URLs of any endpoint that prevents their
update if the endpoint has credentials set.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2025-05-24 22:26:01 +00:00

485 lines
15 KiB
Go

// Copyright 2025 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.
package sql
import (
"context"
"log/slog"
"github.com/pkg/errors"
"gorm.io/gorm"
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
"github.com/cloudbase/garm/auth"
"github.com/cloudbase/garm/database/common"
"github.com/cloudbase/garm/params"
)
func (s *sqlDatabase) CreateGiteaEndpoint(_ context.Context, param params.CreateGiteaEndpointParams) (ghEndpoint params.ForgeEndpoint, err error) {
defer func() {
if err == nil {
s.sendNotify(common.GithubEndpointEntityType, common.CreateOperation, ghEndpoint)
}
}()
var endpoint GithubEndpoint
err = s.conn.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("name = ?", param.Name).First(&endpoint).Error; err == nil {
return errors.Wrap(runnerErrors.ErrDuplicateEntity, "gitea endpoint already exists")
}
endpoint = GithubEndpoint{
Name: param.Name,
Description: param.Description,
APIBaseURL: param.APIBaseURL,
BaseURL: param.BaseURL,
CACertBundle: param.CACertBundle,
EndpointType: params.GiteaEndpointType,
}
if err := tx.Create(&endpoint).Error; err != nil {
return errors.Wrap(err, "creating gitea endpoint")
}
return nil
})
if err != nil {
return params.ForgeEndpoint{}, errors.Wrap(err, "creating gitea endpoint")
}
ghEndpoint, err = s.sqlToCommonGithubEndpoint(endpoint)
if err != nil {
return params.ForgeEndpoint{}, errors.Wrap(err, "converting gitea endpoint")
}
return ghEndpoint, nil
}
func (s *sqlDatabase) ListGiteaEndpoints(_ context.Context) ([]params.ForgeEndpoint, error) {
var endpoints []GithubEndpoint
err := s.conn.Where("endpoint_type = ?", params.GiteaEndpointType).Find(&endpoints).Error
if err != nil {
return nil, errors.Wrap(err, "fetching gitea endpoints")
}
var ret []params.ForgeEndpoint
for _, ep := range endpoints {
commonEp, err := s.sqlToCommonGithubEndpoint(ep)
if err != nil {
return nil, errors.Wrap(err, "converting gitea endpoint")
}
ret = append(ret, commonEp)
}
return ret, nil
}
func (s *sqlDatabase) UpdateGiteaEndpoint(_ context.Context, name string, param params.UpdateGiteaEndpointParams) (ghEndpoint params.ForgeEndpoint, err error) {
defer func() {
if err == nil {
s.sendNotify(common.GithubEndpointEntityType, common.UpdateOperation, ghEndpoint)
}
}()
var endpoint GithubEndpoint
err = s.conn.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("name = ? and endpoint_type = ?", name, params.GiteaEndpointType).First(&endpoint).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrap(runnerErrors.ErrNotFound, "gitea endpoint not found")
}
return errors.Wrap(err, "fetching gitea endpoint")
}
var credsCount int64
if err := tx.Model(&GiteaCredentials{}).Where("endpoint_name = ?", endpoint.Name).Count(&credsCount).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrap(err, "fetching gitea credentials")
}
}
if credsCount > 0 && (param.APIBaseURL != nil || param.BaseURL != nil) {
return errors.Wrap(runnerErrors.ErrBadRequest, "cannot update endpoint URLs with existing credentials")
}
if param.APIBaseURL != nil {
endpoint.APIBaseURL = *param.APIBaseURL
}
if param.BaseURL != nil {
endpoint.BaseURL = *param.BaseURL
}
if param.CACertBundle != nil {
endpoint.CACertBundle = param.CACertBundle
}
if param.Description != nil {
endpoint.Description = *param.Description
}
if err := tx.Save(&endpoint).Error; err != nil {
return errors.Wrap(err, "updating gitea endpoint")
}
return nil
})
if err != nil {
return params.ForgeEndpoint{}, errors.Wrap(err, "updating gitea endpoint")
}
ghEndpoint, err = s.sqlToCommonGithubEndpoint(endpoint)
if err != nil {
return params.ForgeEndpoint{}, errors.Wrap(err, "converting gitea endpoint")
}
return ghEndpoint, nil
}
func (s *sqlDatabase) GetGiteaEndpoint(_ context.Context, name string) (params.ForgeEndpoint, error) {
var endpoint GithubEndpoint
err := s.conn.Where("name = ? and endpoint_type = ?", name, params.GiteaEndpointType).First(&endpoint).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return params.ForgeEndpoint{}, errors.Wrap(runnerErrors.ErrNotFound, "gitea endpoint not found")
}
return params.ForgeEndpoint{}, errors.Wrap(err, "fetching gitea endpoint")
}
return s.sqlToCommonGithubEndpoint(endpoint)
}
func (s *sqlDatabase) DeleteGiteaEndpoint(_ context.Context, name string) (err error) {
defer func() {
if err == nil {
s.sendNotify(common.GithubEndpointEntityType, common.DeleteOperation, params.ForgeEndpoint{Name: name})
}
}()
err = s.conn.Transaction(func(tx *gorm.DB) error {
var endpoint GithubEndpoint
if err := tx.Where("name = ? and endpoint_type = ?", name, params.GiteaEndpointType).First(&endpoint).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return errors.Wrap(err, "fetching gitea endpoint")
}
var credsCount int64
if err := tx.Model(&GiteaCredentials{}).Where("endpoint_name = ?", endpoint.Name).Count(&credsCount).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrap(err, "fetching gitea credentials")
}
}
var repoCnt int64
if err := tx.Model(&Repository{}).Where("endpoint_name = ?", endpoint.Name).Count(&repoCnt).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrap(err, "fetching gitea repositories")
}
}
var orgCnt int64
if err := tx.Model(&Organization{}).Where("endpoint_name = ?", endpoint.Name).Count(&orgCnt).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrap(err, "fetching gitea organizations")
}
}
if credsCount > 0 || repoCnt > 0 || orgCnt > 0 {
return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete endpoint with associated entities")
}
if err := tx.Unscoped().Delete(&endpoint).Error; err != nil {
return errors.Wrap(err, "deleting gitea endpoint")
}
return nil
})
if err != nil {
return errors.Wrap(err, "deleting gitea endpoint")
}
return nil
}
func (s *sqlDatabase) CreateGiteaCredentials(ctx context.Context, param params.CreateGiteaCredentialsParams) (gtCreds params.ForgeCredentials, err error) {
userID, err := getUIDFromContext(ctx)
if err != nil {
return params.ForgeCredentials{}, errors.Wrap(err, "creating gitea credentials")
}
if param.Endpoint == "" {
return params.ForgeCredentials{}, errors.Wrap(runnerErrors.ErrBadRequest, "endpoint name is required")
}
defer func() {
if err == nil {
s.sendNotify(common.GiteaCredentialsEntityType, common.CreateOperation, gtCreds)
}
}()
var creds GiteaCredentials
err = s.conn.Transaction(func(tx *gorm.DB) error {
var endpoint GithubEndpoint
if err := tx.Where("name = ? and endpoint_type = ?", param.Endpoint, params.GiteaEndpointType).First(&endpoint).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrap(runnerErrors.ErrNotFound, "gitea endpoint not found")
}
return errors.Wrap(err, "fetching gitea endpoint")
}
if err := tx.Where("name = ? and user_id = ?", param.Name, userID).First(&creds).Error; err == nil {
return errors.Wrap(runnerErrors.ErrDuplicateEntity, "gitea credentials already exists")
}
var data []byte
var err error
switch param.AuthType {
case params.ForgeAuthTypePAT:
data, err = s.marshalAndSeal(param.PAT)
default:
return errors.Wrap(runnerErrors.ErrBadRequest, "invalid auth type")
}
if err != nil {
return errors.Wrap(err, "marshaling and sealing credentials")
}
creds = GiteaCredentials{
Name: param.Name,
Description: param.Description,
EndpointName: &endpoint.Name,
AuthType: param.AuthType,
Payload: data,
UserID: &userID,
}
if err := tx.Create(&creds).Error; err != nil {
return errors.Wrap(err, "creating gitea credentials")
}
// Skip making an extra query.
creds.Endpoint = endpoint
return nil
})
if err != nil {
return params.ForgeCredentials{}, errors.Wrap(err, "creating gitea credentials")
}
gtCreds, err = s.sqlGiteaToCommonForgeCredentials(creds)
if err != nil {
return params.ForgeCredentials{}, errors.Wrap(err, "converting gitea credentials")
}
return gtCreds, nil
}
func (s *sqlDatabase) getGiteaCredentialsByName(ctx context.Context, tx *gorm.DB, name string, detailed bool) (GiteaCredentials, error) {
var creds GiteaCredentials
q := tx.Preload("Endpoint")
if detailed {
q = q.
Preload("Repositories").
Preload("Organizations").
Preload("Repositories.GiteaCredentials").
Preload("Organizations.GiteaCredentials").
Preload("Repositories.Credentials").
Preload("Organizations.Credentials")
}
userID, err := getUIDFromContext(ctx)
if err != nil {
return GiteaCredentials{}, errors.Wrap(err, "fetching gitea credentials")
}
q = q.Where("user_id = ?", userID)
err = q.Where("name = ?", name).First(&creds).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return GiteaCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "gitea credentials not found")
}
return GiteaCredentials{}, errors.Wrap(err, "fetching gitea credentials")
}
return creds, nil
}
func (s *sqlDatabase) GetGiteaCredentialsByName(ctx context.Context, name string, detailed bool) (params.ForgeCredentials, error) {
creds, err := s.getGiteaCredentialsByName(ctx, s.conn, name, detailed)
if err != nil {
return params.ForgeCredentials{}, errors.Wrap(err, "fetching gitea credentials")
}
return s.sqlGiteaToCommonForgeCredentials(creds)
}
func (s *sqlDatabase) GetGiteaCredentials(ctx context.Context, id uint, detailed bool) (params.ForgeCredentials, error) {
var creds GiteaCredentials
q := s.conn.Preload("Endpoint")
if detailed {
q = q.
Preload("Repositories").
Preload("Organizations").
Preload("Repositories.GiteaCredentials").
Preload("Organizations.GiteaCredentials").
Preload("Repositories.Credentials").
Preload("Organizations.Credentials")
}
if !auth.IsAdmin(ctx) {
userID, err := getUIDFromContext(ctx)
if err != nil {
return params.ForgeCredentials{}, errors.Wrap(err, "fetching gitea credentials")
}
q = q.Where("user_id = ?", userID)
}
err := q.Where("id = ?", id).First(&creds).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return params.ForgeCredentials{}, errors.Wrap(runnerErrors.ErrNotFound, "gitea credentials not found")
}
return params.ForgeCredentials{}, errors.Wrap(err, "fetching gitea credentials")
}
return s.sqlGiteaToCommonForgeCredentials(creds)
}
func (s *sqlDatabase) ListGiteaCredentials(ctx context.Context) ([]params.ForgeCredentials, error) {
q := s.conn.Preload("Endpoint")
if !auth.IsAdmin(ctx) {
userID, err := getUIDFromContext(ctx)
if err != nil {
return nil, errors.Wrap(err, "fetching gitea credentials")
}
q = q.Where("user_id = ?", userID)
}
var creds []GiteaCredentials
err := q.Preload("Endpoint").Find(&creds).Error
if err != nil {
return nil, errors.Wrap(err, "fetching gitea credentials")
}
var ret []params.ForgeCredentials
for _, c := range creds {
commonCreds, err := s.sqlGiteaToCommonForgeCredentials(c)
if err != nil {
return nil, errors.Wrap(err, "converting gitea credentials")
}
ret = append(ret, commonCreds)
}
return ret, nil
}
func (s *sqlDatabase) UpdateGiteaCredentials(ctx context.Context, id uint, param params.UpdateGiteaCredentialsParams) (gtCreds params.ForgeCredentials, err error) {
defer func() {
if err == nil {
s.sendNotify(common.GiteaCredentialsEntityType, common.UpdateOperation, gtCreds)
}
}()
var creds GiteaCredentials
err = s.conn.Transaction(func(tx *gorm.DB) error {
q := tx.Preload("Endpoint")
if !auth.IsAdmin(ctx) {
userID, err := getUIDFromContext(ctx)
if err != nil {
return errors.Wrap(err, "updating gitea credentials")
}
q = q.Where("user_id = ?", userID)
}
if err := q.Where("id = ?", id).First(&creds).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrap(runnerErrors.ErrNotFound, "gitea credentials not found")
}
return errors.Wrap(err, "fetching gitea credentials")
}
if param.Name != nil {
creds.Name = *param.Name
}
if param.Description != nil {
creds.Description = *param.Description
}
var data []byte
var err error
switch creds.AuthType {
case params.ForgeAuthTypePAT:
if param.PAT != nil {
data, err = s.marshalAndSeal(param.PAT)
}
default:
return errors.Wrap(runnerErrors.ErrBadRequest, "invalid auth type")
}
if err != nil {
return errors.Wrap(err, "marshaling and sealing credentials")
}
if len(data) > 0 {
creds.Payload = data
}
if err := tx.Save(&creds).Error; err != nil {
return errors.Wrap(err, "updating gitea credentials")
}
return nil
})
if err != nil {
return params.ForgeCredentials{}, errors.Wrap(err, "updating gitea credentials")
}
gtCreds, err = s.sqlGiteaToCommonForgeCredentials(creds)
if err != nil {
return params.ForgeCredentials{}, errors.Wrap(err, "converting gitea credentials")
}
return gtCreds, nil
}
func (s *sqlDatabase) DeleteGiteaCredentials(ctx context.Context, id uint) (err error) {
var creds GiteaCredentials
defer func() {
if err == nil {
forgeCreds, innerErr := s.sqlGiteaToCommonForgeCredentials(creds)
if innerErr != nil {
slog.ErrorContext(ctx, "converting gitea credentials", "error", innerErr)
}
if creds.ID == 0 || creds.Name == "" {
return
}
s.sendNotify(common.GiteaCredentialsEntityType, common.DeleteOperation, forgeCreds)
}
}()
err = s.conn.Transaction(func(tx *gorm.DB) error {
q := tx.Where("id = ?", id).
Preload("Repositories").
Preload("Organizations")
if !auth.IsAdmin(ctx) {
userID, err := getUIDFromContext(ctx)
if err != nil {
return errors.Wrap(err, "deleting gitea credentials")
}
q = q.Where("user_id = ?", userID)
}
err := q.First(&creds).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return errors.Wrap(err, "fetching gitea credentials")
}
if len(creds.Repositories) > 0 {
return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete credentials with repositories")
}
if len(creds.Organizations) > 0 {
return errors.Wrap(runnerErrors.ErrBadRequest, "cannot delete credentials with organizations")
}
if err := tx.Unscoped().Delete(&creds).Error; err != nil {
return errors.Wrap(err, "deleting gitea credentials")
}
return nil
})
if err != nil {
return errors.Wrap(err, "deleting gitea credentials")
}
return nil
}