2019-05-03 10:47:22 +00:00
// Copyright 2019 Sorint.lab
//
// 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.
2019-05-03 21:35:25 +00:00
package action
2019-05-03 10:47:22 +00:00
import (
"context"
"encoding/json"
"path"
2019-07-01 09:40:20 +00:00
"agola.io/agola/internal/datamanager"
"agola.io/agola/internal/db"
2019-07-31 13:17:54 +00:00
"agola.io/agola/internal/services/configstore/types"
2019-07-01 09:40:20 +00:00
"agola.io/agola/internal/util"
2019-05-03 10:47:22 +00:00
uuid "github.com/satori/go.uuid"
2019-05-23 09:23:14 +00:00
errors "golang.org/x/xerrors"
2019-05-03 10:47:22 +00:00
)
2019-05-09 13:32:27 +00:00
func ( h * ActionHandler ) ValidateProject ( ctx context . Context , project * types . Project ) error {
2019-05-03 10:47:22 +00:00
if project . Name == "" {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "project name required" ) )
2019-05-03 10:47:22 +00:00
}
if ! util . ValidateName ( project . Name ) {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "invalid project name %q" , project . Name ) )
2019-05-03 10:47:22 +00:00
}
if project . Parent . ID == "" {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "project parent id required" ) )
2019-05-03 10:47:22 +00:00
}
if project . Parent . Type != types . ConfigTypeProjectGroup {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "invalid project parent type %q" , project . Parent . Type ) )
2019-05-03 10:47:22 +00:00
}
if ! types . IsValidVisibility ( project . Visibility ) {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "invalid project visibility" ) )
2019-05-03 10:47:22 +00:00
}
if ! types . IsValidRemoteRepositoryConfigType ( project . RemoteRepositoryConfigType ) {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "invalid project remote repository config type %q" , project . RemoteRepositoryConfigType ) )
2019-05-03 10:47:22 +00:00
}
if project . RemoteRepositoryConfigType == types . RemoteRepositoryConfigTypeRemoteSource {
if project . RemoteSourceID == "" {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "empty remote source id" ) )
2019-05-03 10:47:22 +00:00
}
if project . LinkedAccountID == "" {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "empty linked account id" ) )
2019-05-03 10:47:22 +00:00
}
if project . RepositoryID == "" {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "empty remote repository id" ) )
2019-05-03 10:47:22 +00:00
}
if project . RepositoryPath == "" {
2019-05-09 13:32:27 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "empty remote repository path" ) )
2019-05-03 10:47:22 +00:00
}
}
2019-05-09 13:32:27 +00:00
return nil
}
func ( h * ActionHandler ) CreateProject ( ctx context . Context , project * types . Project ) ( * types . Project , error ) {
if err := h . ValidateProject ( ctx , project ) ; err != nil {
return nil , err
}
2019-05-03 10:47:22 +00:00
var cgt * datamanager . ChangeGroupsUpdateToken
// must do all the checks in a single transaction to avoid concurrent changes
2019-07-25 08:46:02 +00:00
err := h . readDB . Do ( ctx , func ( tx * db . Tx ) error {
2019-05-03 10:47:22 +00:00
var err error
2019-05-03 21:35:25 +00:00
group , err := h . readDB . GetProjectGroup ( tx , project . Parent . ID )
2019-05-03 10:47:22 +00:00
if err != nil {
return err
}
if group == nil {
return util . NewErrBadRequest ( errors . Errorf ( "project group with id %q doesn't exist" , project . Parent . ID ) )
}
project . Parent . ID = group . ID
2019-05-03 21:35:25 +00:00
groupPath , err := h . readDB . GetProjectGroupPath ( tx , group )
2019-05-03 10:47:22 +00:00
if err != nil {
return err
}
pp := path . Join ( groupPath , project . Name )
// changegroup is the project path. Use "projectpath" prefix as it must
// cover both projects and projectgroups
cgNames := [ ] string { util . EncodeSha256Hex ( "projectpath-" + pp ) }
2019-05-03 21:35:25 +00:00
cgt , err = h . readDB . GetChangeGroupsUpdateTokens ( tx , cgNames )
2019-05-03 10:47:22 +00:00
if err != nil {
return err
}
// check duplicate project name
2019-05-03 21:35:25 +00:00
p , err := h . readDB . GetProjectByName ( tx , project . Parent . ID , project . Name )
2019-05-03 10:47:22 +00:00
if err != nil {
return err
}
if p != nil {
return util . NewErrBadRequest ( errors . Errorf ( "project with name %q, path %q already exists" , p . Name , pp ) )
}
if project . RemoteRepositoryConfigType == types . RemoteRepositoryConfigTypeRemoteSource {
// check that the linked account matches the remote source
2019-05-03 21:35:25 +00:00
user , err := h . readDB . GetUserByLinkedAccount ( tx , project . LinkedAccountID )
2019-05-03 10:47:22 +00:00
if err != nil {
2019-05-23 09:23:14 +00:00
return errors . Errorf ( "failed to get user with linked account id %q: %w" , project . LinkedAccountID , err )
2019-05-03 10:47:22 +00:00
}
if user == nil {
return util . NewErrBadRequest ( errors . Errorf ( "user for linked account %q doesn't exist" , project . LinkedAccountID ) )
}
la , ok := user . LinkedAccounts [ project . LinkedAccountID ]
if ! ok {
return util . NewErrBadRequest ( errors . Errorf ( "linked account id %q for user %q doesn't exist" , project . LinkedAccountID , user . Name ) )
}
if la . RemoteSourceID != project . RemoteSourceID {
return util . NewErrBadRequest ( errors . Errorf ( "linked account id %q remote source %q different than project remote source %q" , project . LinkedAccountID , la . RemoteSourceID , project . RemoteSourceID ) )
}
}
return nil
} )
if err != nil {
return nil , err
}
project . ID = uuid . NewV4 ( ) . String ( )
project . Parent . Type = types . ConfigTypeProjectGroup
2019-05-07 16:29:31 +00:00
// generate the Secret and the WebhookSecret
2019-05-07 15:16:42 +00:00
project . Secret = util . EncodeSha1Hex ( uuid . NewV4 ( ) . String ( ) )
2019-05-07 16:29:31 +00:00
project . WebhookSecret = util . EncodeSha1Hex ( uuid . NewV4 ( ) . String ( ) )
2019-05-03 10:47:22 +00:00
pcj , err := json . Marshal ( project )
if err != nil {
2019-05-23 09:23:14 +00:00
return nil , errors . Errorf ( "failed to marshal project: %w" , err )
2019-05-03 10:47:22 +00:00
}
actions := [ ] * datamanager . Action {
{
ActionType : datamanager . ActionTypePut ,
DataType : string ( types . ConfigTypeProject ) ,
ID : project . ID ,
Data : pcj ,
} ,
}
2019-05-03 21:35:25 +00:00
_ , err = h . dm . WriteWal ( ctx , actions , cgt )
2019-05-03 10:47:22 +00:00
return project , err
}
2019-05-09 13:33:57 +00:00
type UpdateProjectRequest struct {
ProjectRef string
Project * types . Project
}
func ( h * ActionHandler ) UpdateProject ( ctx context . Context , req * UpdateProjectRequest ) ( * types . Project , error ) {
if err := h . ValidateProject ( ctx , req . Project ) ; err != nil {
return nil , err
}
var cgt * datamanager . ChangeGroupsUpdateToken
// must do all the checks in a single transaction to avoid concurrent changes
2019-07-25 08:46:02 +00:00
err := h . readDB . Do ( ctx , func ( tx * db . Tx ) error {
2019-05-09 13:33:57 +00:00
var err error
// check project exists
p , err := h . readDB . GetProject ( tx , req . ProjectRef )
if err != nil {
return err
}
if p == nil {
return util . NewErrBadRequest ( errors . Errorf ( "project with ref %q doesn't exist" , req . ProjectRef ) )
}
// check that the project.ID matches
if p . ID != req . Project . ID {
return util . NewErrBadRequest ( errors . Errorf ( "project with ref %q has a different id" , req . ProjectRef ) )
}
// check parent project group exists
group , err := h . readDB . GetProjectGroup ( tx , req . Project . Parent . ID )
if err != nil {
return err
}
if group == nil {
return util . NewErrBadRequest ( errors . Errorf ( "project group with id %q doesn't exist" , req . Project . Parent . ID ) )
}
// currently we don't support changing parent
// TODO(sgotti) handle project move (changed parent project group)
if p . Parent . ID != req . Project . Parent . ID {
return util . NewErrBadRequest ( errors . Errorf ( "changing project parent isn't supported" ) )
}
2019-07-08 16:12:42 +00:00
groupPath , err := h . readDB . GetProjectGroupPath ( tx , group )
2019-05-09 13:33:57 +00:00
if err != nil {
return err
}
2019-07-08 16:12:42 +00:00
pp := path . Join ( groupPath , req . Project . Name )
2019-05-09 13:33:57 +00:00
2019-07-08 16:12:42 +00:00
if p . Name != req . Project . Name {
// check duplicate project name
ap , err := h . readDB . GetProjectByName ( tx , req . Project . Parent . ID , req . Project . Name )
if err != nil {
return err
}
if ap != nil {
2019-07-25 08:46:02 +00:00
return util . NewErrBadRequest ( errors . Errorf ( "project with name %q, path %q already exists" , req . Project . Name , pp ) )
2019-07-08 16:12:42 +00:00
}
2019-05-09 13:33:57 +00:00
}
// changegroup is the project path. Use "projectpath" prefix as it must
// cover both projects and projectgroups
cgNames := [ ] string { util . EncodeSha256Hex ( "projectpath-" + pp ) }
cgt , err = h . readDB . GetChangeGroupsUpdateTokens ( tx , cgNames )
if err != nil {
return err
}
if req . Project . RemoteRepositoryConfigType == types . RemoteRepositoryConfigTypeRemoteSource {
// check that the linked account matches the remote source
user , err := h . readDB . GetUserByLinkedAccount ( tx , req . Project . LinkedAccountID )
if err != nil {
2019-05-23 09:23:14 +00:00
return errors . Errorf ( "failed to get user with linked account id %q: %w" , req . Project . LinkedAccountID , err )
2019-05-09 13:33:57 +00:00
}
if user == nil {
return util . NewErrBadRequest ( errors . Errorf ( "user for linked account %q doesn't exist" , req . Project . LinkedAccountID ) )
}
la , ok := user . LinkedAccounts [ req . Project . LinkedAccountID ]
if ! ok {
return util . NewErrBadRequest ( errors . Errorf ( "linked account id %q for user %q doesn't exist" , req . Project . LinkedAccountID , user . Name ) )
}
if la . RemoteSourceID != req . Project . RemoteSourceID {
return util . NewErrBadRequest ( errors . Errorf ( "linked account id %q remote source %q different than project remote source %q" , req . Project . LinkedAccountID , la . RemoteSourceID , req . Project . RemoteSourceID ) )
}
}
return nil
} )
if err != nil {
return nil , err
}
pcj , err := json . Marshal ( req . Project )
if err != nil {
2019-05-23 09:23:14 +00:00
return nil , errors . Errorf ( "failed to marshal project: %w" , err )
2019-05-09 13:33:57 +00:00
}
actions := [ ] * datamanager . Action {
{
ActionType : datamanager . ActionTypePut ,
DataType : string ( types . ConfigTypeProject ) ,
ID : req . Project . ID ,
Data : pcj ,
} ,
}
_ , err = h . dm . WriteWal ( ctx , actions , cgt )
return req . Project , err
}
2019-05-03 21:35:25 +00:00
func ( h * ActionHandler ) DeleteProject ( ctx context . Context , projectRef string ) error {
2019-05-03 10:47:22 +00:00
var project * types . Project
var cgt * datamanager . ChangeGroupsUpdateToken
// must do all the checks in a single transaction to avoid concurrent changes
2019-07-25 08:46:02 +00:00
err := h . readDB . Do ( ctx , func ( tx * db . Tx ) error {
2019-05-03 10:47:22 +00:00
var err error
// check project existance
2019-05-03 21:35:25 +00:00
project , err = h . readDB . GetProject ( tx , projectRef )
2019-05-03 10:47:22 +00:00
if err != nil {
return err
}
if project == nil {
return util . NewErrBadRequest ( errors . Errorf ( "project %q doesn't exist" , projectRef ) )
}
// changegroup is the project id.
cgNames := [ ] string { util . EncodeSha256Hex ( project . ID ) }
2019-05-03 21:35:25 +00:00
cgt , err = h . readDB . GetChangeGroupsUpdateTokens ( tx , cgNames )
2019-05-03 10:47:22 +00:00
if err != nil {
return err
}
return nil
} )
if err != nil {
return err
}
2019-05-12 22:23:57 +00:00
// TODO(sgotti) implement childs garbage collection
2019-05-03 10:47:22 +00:00
actions := [ ] * datamanager . Action {
{
ActionType : datamanager . ActionTypeDelete ,
DataType : string ( types . ConfigTypeProject ) ,
ID : project . ID ,
} ,
}
2019-05-03 21:35:25 +00:00
_ , err = h . dm . WriteWal ( ctx , actions , cgt )
2019-05-03 10:47:22 +00:00
return err
}